ControlForge Project Snapshots
June 2026 | ControlForge v1.0.1069
A project snapshot is a point-in-time, content-addressed capture of your running project — programs, tasks, config, I/O mapping — frozen and stored under a short hash. Snapshots give you a version-history timeline you can browse, diff against, and restore from: take one before a risky edit, before a deploy, or on a schedule, and you always have a known-good point to roll back to.
Snapshots are content-addressed: the hash is derived from the project content, so capturing the same state twice yields the same hash (you'll see one hash referenced by multiple history entries). Each stored snapshot keeps the raw project JSON plus a gzip-compressed copy for size, and is tagged with a source so you can tell why it was taken.
One naming caveat. There is an ST function literally named
SNAPSHOT, but it is not the project-snapshot tool — it belongs to the in-runtime test framework and records a(name, value)pair into a per-test trace. It is covered at the end of this guide so you don't reach for it by mistake. Project snapshots are an API/UI feature (/api/snapshots), not an ST call.
How a snapshot is identified
| Field | Meaning |
|---|---|
hash | Short content hash — the snapshot's ID in every endpoint path |
name | The project name captured |
created_at | When it was taken (UTC) |
source | Why it was taken — e.g. manual, download, or a custom tag you pass |
size_raw / size_gz | Stored project size, raw and gzip-compressed (bytes) |
Creating a snapshot
POST /api/snapshots captures the current running state. The body is optional — a bodyless POST works and defaults source to manual. Pass {"source":"..."} to tag why you took it (the tag is trimmed; empty falls back to manual).
# Grab a token
TOKEN=$(curl -s -X POST http://localhost:8302/api/auth/login \
-H 'Content-Type: application/json' \
-d '{"username":"goplc","password":"goplc"}' | python3 -c 'import sys,json;print(json.load(sys.stdin)["token"])')
# Capture, tagging the reason
curl -s -X POST http://localhost:8302/api/snapshots \
-H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
-d '{"source":"pre-deploy"}'
{ "hash": "eec6af70", "name": "counter1", "source": "pre-deploy", "success": true }
A bodyless capture (defaults source to manual):
curl -s -X POST http://localhost:8302/api/snapshots -H "Authorization: Bearer $TOKEN"
Browsing snapshots and history
There are two views: the snapshot list (distinct stored snapshots) and the history (the timeline of which hash was active when).
List stored snapshots — GET /api/snapshots, newest first. Add ?name=<project> to scope to one project, ?limit=N to cap the count:
curl -s http://localhost:8302/api/snapshots -H "Authorization: Bearer $TOKEN"
{
"snapshots": [
{
"hash": "eec6af70", "name": "counter1",
"created_at": "2026-06-22T04:46:24Z",
"source": "pre-deploy", "size_raw": 14332, "size_gz": 2110
},
{
"hash": "abaaf88a", "name": "counter1",
"created_at": "2026-06-18T20:17:35Z",
"source": "download", "size_raw": 14686, "size_gz": 2283
}
]
}
View the history timeline — GET /api/snapshots/history, newest first. Each entry records the node, the active hash, the timestamp, and the runtime version at that moment. Scope with ?name=<project>, ?node_id=<node>, ?limit=N:
curl -s http://localhost:8302/api/snapshots/history -H "Authorization: Bearer $TOKEN"
{
"history": [
{ "id": 11, "node_id": "node-a:8302", "hash": "eec6af70",
"timestamp": "2026-06-22T04:46:24Z", "version": "1.0.1069" },
{ "id": 10, "node_id": "node-a:8302", "hash": "abaaf88a",
"timestamp": "2026-06-18T22:13:08Z", "version": "1.0.1056" }
]
}
Because snapshots are content-addressed, the same
hashcan appear in several history entries — that's the same project content re-captured at different times, not a duplicate.
Fetch a snapshot's full project JSON — GET /api/snapshots/{hash} streams the stored project verbatim (this is what restore will apply):
curl -s http://localhost:8302/api/snapshots/eec6af70 -H "Authorization: Bearer $TOKEN" | head -c 200
{ "version": "1.7", "metadata": { "name": "counter1", "created": "2026-06-22T00:46:24..." }, "programs": {}, "config_yaml": "io_mapping:\n ..." }
Restoring by hash
POST /api/snapshots/{hash}/restore restores that snapshot as the current project. The restore stages the project — you then Download (apply) it to push it into the live runtime, which is why the response says so:
curl -s -X POST http://localhost:8302/api/snapshots/eec6af70/restore -H "Authorization: Bearer $TOKEN"
{
"hash": "eec6af70",
"message": "Snapshot restored — Download to apply to runtime",
"name": "counter1",
"program_count": 0,
"success": true
}
Restore brings back the captured project content. To make it live, apply it (Download) the same way you'd push any project change — restore itself does not hot-swap the running tasks. Take a fresh snapshot before restoring if the current state isn't already captured, so you can roll forward again.
Deleting a snapshot
DELETE /api/snapshots/{hash} removes a stored snapshot:
curl -s -X DELETE http://localhost:8302/api/snapshots/eec6af70 -H "Authorization: Bearer $TOKEN"
{ "message": "Snapshot deleted", "success": true }
Deleting a snapshot removes the stored content; existing history entries that referenced its hash remain as a record of when it was active.
Endpoint reference
| Method & path | Purpose |
|---|---|
GET /api/snapshots | List stored snapshots (?name, ?limit) |
POST /api/snapshots | Capture current state (optional {"source":"..."}) |
GET /api/snapshots/history | Timeline of which hash was active when (?name, ?node_id, ?limit) |
GET /api/snapshots/{hash} | Full project JSON for a snapshot |
POST /api/snapshots/{hash}/restore | Restore a snapshot (then Download to apply) |
DELETE /api/snapshots/{hash} | Delete a snapshot by hash |
All endpoints require a JWT (Authorization: Bearer <token>); the tag in the OpenAPI spec is snapshots.
The SNAPSHOT ST function — a different thing
Searching the live function registry for SNAPSHOT (GET /api/docs/functions?search=SNAPSHOT) returns a built-in — but it belongs to the TEST category and has nothing to do with project snapshots:
SNAPSHOT(name : STRING, value : ANY) : BOOL
Record (name, value) into the per-test snapshot trace. Runner compares against
__snapshots__/<test>.snapon subsequent runs; rewrite with--update-snapshots.
This is snapshot testing — you call SNAPSHOT('label', someValue) inside a test POU, and the first run records the value as the golden reference; later runs fail if the value drifts. It is a regression-testing tool, not a way to capture or restore a project. For point-in-time project capture/restore, use the /api/snapshots endpoints above — there is no ST function for that.
// In a test POU — captures a golden value the runner diffs on later runs.
PROGRAM Test_Counter
VAR
total : DINT := 42;
END_VAR
SNAPSHOT('counter_total', total); // first run records 42 as the baseline
END_PROGRAM
Notes & limits
- Content-addressed — identical project content yields the same
hash; one hash can appear across multiple history entries. Snapshots are deduplicated by content, not by capture time. - Restore stages, Download applies — a restore makes the snapshot the current project but does not hot-swap running tasks; apply it (Download) to push it live. Snapshot the current state first if it isn't already captured.
sourceis a free-form reason tag —manual(default),download, or whatever you pass; use it to tell automated captures apart from manual ones in the list.- History outlives snapshots — deleting a snapshot removes its stored content but leaves the history entries that referenced its hash, so the timeline of when a hash was active is preserved.
SNAPSHOT(ST) ≠ project snapshot — the STSNAPSHOT(name, value)built-in is the test framework's golden-value recorder. Don't reach for it to capture or restore a project; that's the/api/snapshotssurface.