# \[IGN-14073\] 8.3 replace for HistoryManager::storeHistory

**URL:** <https://forum.inductiveautomation.com/t/ign-14073-8-3-replace-for-historymanager-storehistory/110189>\
**Category:** Ignition 8.3 Early Access\
**Created:** [August 26, 2025, 7:39pm UTC](https://forum.inductiveautomation.com/t/ign-14073-8-3-replace-for-historymanager-storehistory/110189 "2025-08-26T19:39:46Z")\
**Posts on this page:** 8\
**Page:** 1

<div class="post-metadata">

**Author:** ![mazeyrat](https://sea1.discourse-cdn.com/inductiveautomation/user_avatar/forum.inductiveautomation.com/mazeyrat/32/10955_2.png) [@mazeyrat](https://forum.inductiveautomation.com/u/mazeyrat)\
**Post date:** [August 26, 2025, 7:39pm UTC](https://forum.inductiveautomation.com/t/ign-14073-8-3-replace-for-historymanager-storehistory/110189/1 "2025-08-26T19:39:46Z")

</div>

> [@Store tag history from a module](https://forum.inductiveautomation.com/t/store-tag-history-from-a-module/108820/2):
>
> As you noted, storing Historian data has been moved into the Historian module rather than coupled with the Store & Forward system (old HistoryManager, now called StoreAndForwardManager) for storage. The most correct method would be to grab the Historian's StorageEngine using HistorianManager.get(gatewayContext).getStorageEngine(historianName), and then to store AtomicPoints through StorageEngine::storeAtomic. Creating atomic points can be done using DataPointFactory::createAtomicPoint. You sho…

@Kurt.Larson I have a 8.1 module using the old HistoryManager to store some AuditRecordSFData with:

```

```java
context.getHistoryManager()
        .storeHistory(ds.getName(), new AuditRecordSFData(ds.getName(), insertQuery,
                record.getTimestamp(), record.getOriginatingSystem(),
                record.getOriginatingContext(), record.getActor(), record.getActorHost(),
                record.getAction(),
                record.getActionTarget(),
                actionTargetEquipement,
                ActionTargetDesignation,
                actionValue,
                actionValueDetail,
                record.getStatusCode()));

```

```

```  
public class AuditRecordSFData implements DatasourceData {

```java
String query;
String datasource;
Object[] values;

public AuditRecordSFData(String datasource, String query, Object... values) {
    this.datasource = datasource;
    this.query = query;
    this.values = values;
}

	@Override
	public int getDataCount() {
		return 1;
	}

	@Override
	public HistoryFlavor getFlavor() {
        return DatasourceData.SQLTAG;
	}

	@Override
	public String getLoggerName() {
		return "Audit Log Splitter Store & Forward";
	}

    @Override
	public String getSignature() {
		return "SFQuery: " + datasource + " - " + query;
	}

	@Override
	public void storeToConnection(SRConnection conn) throws Exception {
		try{
			conn.runPrepUpdate(query, values);
		} catch (Exception e) {
			logger.error("storeToConnection - query = {} values = {}",query,values);
			throw e;
		}
	}
}

```

```

what is the right migration path ?

```  
//import com.inductiveautomation.ignition.gateway.history.DatasourceData; //import [//com.inductiveautomation.ignition.gateway.history.HistoryFlavor](https://com.inductiveautomation.ignition.gateway.history.HistoryFlavor); import com.inductiveautomation.ignition.gateway.storeforward.deprecated.DatasourceData; import com.inductiveautomation.ignition.gateway.storeforward.deprecated.HistoryFlavor;  
```

and context.getStoreAndForwardManager().storeData() ?

but in that case, how to obtain a storageId ?

---

<div class="post-metadata">

**Author:** ![Kurt.Larson](https://sea1.discourse-cdn.com/inductiveautomation/user_avatar/forum.inductiveautomation.com/kurt.larson/32/6426_2.png) [@Kurt.Larson](https://forum.inductiveautomation.com/u/Kurt.Larson)\
**Post date:** [August 26, 2025, 8:45pm UTC](https://forum.inductiveautomation.com/t/ign-14073-8-3-replace-for-historymanager-storehistory/110189/2 "2025-08-26T20:45:07Z")

</div>

Yeah, that's correct. The `StorageKey` is a 2 part key - 1) `engineId` and 2) `sinkId`.

So that would be the datasource name (which has an associated engine) and the audit profile name.

Here's an example:

```python
var auditRecord = DefaultAuditRecord.newBuilder()
                .action(record.getAction())
                .actionTarget(record.getActionTarget())
                .actionValue(record.getActionValue())
                .actor(record.getActor())
                .actorHost(record.getActorHost())
                .originatingContext(record.getOriginatingContext())
                .originatingSystem(record.getOriginatingSystem())
                .statusCode(record.getStatusCode())
                .timestamp(record.getTimestamp())
                .build();
var persistentRecord = PersistentAuditRecord.wrap(auditRecord);
var storageKey = StorageKey.of("datasourceName", "auditProfileName");

context.getStoreAndForwardManager().storeData(storageKey, persistentRecord);

```

If you don't know the actual audit profile name, but you have the datasource name, then you could do the below to grab the identifiers for the audit profile associated with that datasource / engine. I should make this easier; there's an `EngineManifest` but it's not accessible from the `StoreAndForwardManager`. For now though:

```python
 /**
 * Retrieves the set of storage keys for audit records associated with the specified store-and-forward engine.
 * This method filters pipeline information tied to the engine to identify storage keys
 * that correspond to audit records.
 *
 * @param engineId the unique identifier of the store-and-forward engine from which audit storage keys are to be retrieved
 * @return a set of {@link StorageKey} objects representing storage destinations for audit records within the specified engine
 */
public Set<StorageKey> retrieveAuditStorageKeys(String engineId) {
    var engineInformation = context.getStoreAndForwardManager().getEngineInformation(engineId, false);

    return engineInformation.stream()
        .map(EngineInformation::pipelineInformation)
        .flatMap(List::stream)
        .filter(pi -> PersistentAuditRecord.FLAVOR.equals(pi.flavor()))
        .map(PipelineInformation::storageKey)
        .collect(Collectors.toSet());
}

```

**Relevant Docs**

`StoreAndForwardManager`

```python
/**
 * Stores the provided persistent data associated with a specific storage key.
 * This is used to persist data into a store-and-forward system.
 *
 * @param storageKey the key identifying the storage destination and sink
 * @param data the persistent data to store
 * @param <T> the type of data to store, which must implement {@link PersistentData}
 * @throws Exception if an error occurs while storing the data
 */
default <T extends PersistentData> void storeData(StorageKey storageKey, T data) throws Exception {
    storeData(PersistentDataBundle.of(storageKey, data));
}

/**
 * Gets descriptive {@link EngineInformation} about the specified engine that is registered with this manager.
 * Quarantined data info will only be included if the specific {@param includeQuarantinedInfo} is {@code true}.
 *
 * @param engineId the id of the engine to get information for
 * @param includeQuarantinedInfo if {@link QuarantinedDataInfo} results should be included
 * @return an optional containing the engine information if it exists
 */
Optional<EngineInformation> getEngineInformation(String engineId, boolean includeQuarantinedInfo);

```

`StorageKey`

```python
/**
 * A composite key that identifies storage destinations and sinks within the store-and-forward system.
 *
 * <p>This key serves dual purposes:
 * <ul>
 * <li>Registration key for data sinks within engines</li>
 * <li>Routing key for associating data with storage destinations</li>
 * </ul>
 *
 * <p>All storage destinations require both an engine ID and a sink ID. The sink ID
 * can be the same as the engine ID to indicate the primary/default storage location.</p>
 *
 * @param engineId identifies the target engine
 * @param sinkId identifies the specific sink within the engine
 */
public record StorageKey(
    String engineId,
    String sinkId
)

/**
 * Creates a StorageKey where the same ID is used for both engine and sink.
 * This represents the primary/default storage location within the engine.
 *
 * @param id the identifier to use for both engine and sink
 * @return new StorageKey instance with the same ID for both components
 * @throws IllegalArgumentException if id is null or blank
 */
public static StorageKey repeated(String id) {
    return new StorageKey(id, id);
}

/**
 * Creates a StorageKey with separate engine and sink identifiers.
 * This allows routing data to specific named sinks within an engine.
 *
 * @param engineId the engine identifier
 * @param sinkId the sink identifier
 * @return new StorageKey instance with separate engine and sink IDs
 * @throws IllegalArgumentException if engineId or sinkId is null or blank
 */
public static StorageKey of(String engineId, String sinkId) {
    return new StorageKey(engineId, sinkId);
}

```

---

<div class="post-metadata">

**Author:** ![Kurt.Larson](https://sea1.discourse-cdn.com/inductiveautomation/user_avatar/forum.inductiveautomation.com/kurt.larson/32/6426_2.png) [@Kurt.Larson](https://forum.inductiveautomation.com/u/Kurt.Larson)\
**Post date:** [August 26, 2025, 9:00pm UTC](https://forum.inductiveautomation.com/t/ign-14073-8-3-replace-for-historymanager-storehistory/110189/3 "2025-08-26T21:00:14Z")

</div>

Oh wait, I'll leave the previous post up for posterity, but I see now that you're using `DatasourceData`. `DatasourceData.SQLTAG` doesn't exist anymore...

There's a `DataSink` called `DatasourceQuerySink` that takes in `QuerySFData` that looks identical to what you have above in your `AuditRecordSFData`.

```python
public QuerySFData(String query, Object[] values, String datasource) {
    this.query = query;
    this.values = values;
    this.datasource = datasource;
}

```

I'm going to move that into `gateway-api` so that you can leverage that. Then you won't need your `AuditRecordSFData` class. You will need to handle potential upgrades from the 8.1 `DatasourceData`, which could look something like this:

```python
// ... include existing AuditRecordSFData structure

@Override
public PersistentData upgrade(String engineId) {
    return new QuerySFData(query, values, datasource)
}

```

**Upgrade method on legacy `HistoricalData`:**

```python
/**
 * Attempts to upgrade the historical data to persistent data using the specified engine ID if necessary.
 *
 * @param engineId the engine ID to use for the upgrade if necessary
 * @return the upgraded persistent data
 */
PersistentData upgrade(String engineId);

```

And then since you're storing simple data as a query then you could just use `StorageKey.repeated(datasourceName)` when storing data to the `StoreAndForwardManager`. That is what the `DatasourceQuerySink` `storageKey` is:

```python
public DatasourceQuerySink(GatewayContext context, String dataSource) {
    super(context, StorageKey.repeated(dataSource), QuerySFData.FLAVOR, 1);
}

```

---

<div class="post-metadata">

**Author:** ![pturmel](https://sea1.discourse-cdn.com/inductiveautomation/user_avatar/forum.inductiveautomation.com/pturmel/32/3295_2.png) [@pturmel](https://forum.inductiveautomation.com/u/pturmel)\
**Post date:** [August 26, 2025, 9:36pm UTC](https://forum.inductiveautomation.com/t/ign-14073-8-3-replace-for-historymanager-storehistory/110189/4 "2025-08-26T21:36:34Z")

</div>

Heh. I'm glad I held off on a custom historian I've been contemplating..... 😅

---

<div class="post-metadata">

**Author:** ![Kurt.Larson](https://sea1.discourse-cdn.com/inductiveautomation/user_avatar/forum.inductiveautomation.com/kurt.larson/32/6426_2.png) [@Kurt.Larson](https://forum.inductiveautomation.com/u/Kurt.Larson)\
**Post date:** [August 26, 2025, 10:05pm UTC](https://forum.inductiveautomation.com/t/ign-14073-8-3-replace-for-historymanager-storehistory/110189/5 "2025-08-26T22:05:44Z")

</div>

Well hopefully creating a `Historian` is easier than before. 😰

Feel free to message me / start another post if/when you do. I'll try to get up some SDK examples for both Store & Forward and Historian.

---

<div class="post-metadata">

**Author:** ![pturmel](https://sea1.discourse-cdn.com/inductiveautomation/user_avatar/forum.inductiveautomation.com/pturmel/32/3295_2.png) [@pturmel](https://forum.inductiveautomation.com/u/pturmel)\
**Post date:** [August 26, 2025, 10:07pm UTC](https://forum.inductiveautomation.com/t/ign-14073-8-3-replace-for-historymanager-storehistory/110189/6 "2025-08-26T22:07:50Z")

</div>

I'm pretty sure I'm going to cheat with a tag actor when it comes up again. 😇

But dodging that migration hassle was my point. Not a ding on the new historian API.

---

<div class="post-metadata">

**Author:** ![mazeyrat](https://sea1.discourse-cdn.com/inductiveautomation/user_avatar/forum.inductiveautomation.com/mazeyrat/32/10955_2.png) [@mazeyrat](https://forum.inductiveautomation.com/u/mazeyrat)\
**Post date:** [September 22, 2025, 5:32pm UTC](https://forum.inductiveautomation.com/t/ign-14073-8-3-replace-for-historymanager-storehistory/110189/7 "2025-09-22T17:32:30Z")

</div>

Thanks a lot for all these explantations !

In my module use case, my audit profil use custom audit table.  
That's Ignition audit table with additionnal fields populated by this module.

I see how to create my audit type by extending the **AuditProfileType.**  
I don't wan't to directly insert into database table, bypassing the store and forward.

So I try to use the **store and forward with a custom audit table**.

Can you confirm I have to use **BasicHistoricalRecord** or correct me ?

```java
BasicHistoricalRecord bhr = new BasicHistoricalRecord("auditCustomTable", "logger");
bhr.addColumn("field1", new BasicQualifiedValue(...));
bhr.addColumn("field2", new BasicQualifiedValue(...));

StorageId storageid = new StorageId(engineId,sinkId);

context.getStoreAndForwardManager().storeData(storageid,bhr);

```

Can't found **StorageKey**? the module need a dependency ?

---

<div class="post-metadata">

**Author:** ![mazeyrat](https://sea1.discourse-cdn.com/inductiveautomation/user_avatar/forum.inductiveautomation.com/mazeyrat/32/10955_2.png) [@mazeyrat](https://forum.inductiveautomation.com/u/mazeyrat)\
**Post date:** [September 23, 2025, 12:10pm UTC](https://forum.inductiveautomation.com/t/ign-14073-8-3-replace-for-historymanager-storehistory/110189/8 "2025-09-23T12:10:40Z")

</div>

According to the doc:

### Store and Forward Connection Policy

Database connections by default have a pool of eight connections. The Store and Forward engine is multi-threaded, meaning the engine can take advantage of more than one of these connections at a time. This also means that if the Store and Forward engine is storing a particular type of data, such as tag history, that action will not block the engine from concurrently storing a different type of data, such as Alarm Journal or Audit Profile data.

There are six different types of data that can be stored and forwarded, meaning up to six of your available connections can be used at the same time. Each available data type is listed below:

- Alarming
- Auditing
- **Scripting**
- SECS/GEM
- Tag history
- Transaction Group

I suppose that my module use case is similar to **scripting** feature  
available with **system.db.runSFPrepUpdate(query, args, datasources)**

if you can share some code to achieve this from a module point of view...
