Skip to main content

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

FieldMeaning
hashShort content hash — the snapshot's ID in every endpoint path
nameThe project name captured
created_atWhen it was taken (UTC)
sourceWhy it was taken — e.g. manual, download, or a custom tag you pass
size_raw / size_gzStored 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 snapshotsGET /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 timelineGET /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 hash can 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 JSONGET /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 & pathPurpose
GET /api/snapshotsList stored snapshots (?name, ?limit)
POST /api/snapshotsCapture current state (optional {"source":"..."})
GET /api/snapshots/historyTimeline of which hash was active when (?name, ?node_id, ?limit)
GET /api/snapshots/{hash}Full project JSON for a snapshot
POST /api/snapshots/{hash}/restoreRestore 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>.snap on 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.
  • source is a free-form reason tagmanual (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 ST SNAPSHOT(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/snapshots surface.