Version History and Rollback
A workflow has a draft (the nodes and edges that updateWorkflow writes) and a published graph (what sessions run). Every publishWorkflow that changes the published graph is also kept as a numbered version. You can list those versions, read any of them in full, see what changed between two of them (or between one and your draft), publish an old one again, or copy an old one into the draft to edit it.
Every operation on this page takes a dashboard JWT (Authorization: Bearer <jwt>) and the x-huat-platform: customer header, like publishWorkflow. An API key is not accepted. Any user of the workspace can call them.
How versions are recorded
Section titled “How versions are recorded”- One entry per change to the published graph. A publish, a rollback, or hiring an agent template that publishes a workflow adds an entry. Publishing a graph identical to the one live now adds none (the
noteis then ignored). versionNumbercounts 1, 2, 3 … per workflow and is never reused. It is not the same asWorkflowDto.version, which is a save counter that goes up on everyupdateWorkflowand everypublishWorkflow.idis the version’s server-wide id.WorkflowDto.currentVersionIdandWorkflowRunDto.workflowVersionIdhold thisid, not theversionNumber. The version operations takeversionNumber.isCurrentmarks the version that new session turns run now (the onecurrentVersionIdpoints to).sourcesays how the entry was created:PUBLISH(publishWorkflow),ROLLBACK(rollbackWorkflow;restoredFromVersionNumbernames the version it republished),HIRE(an agent template hire),SEED(a platform seeding script) orBACKFILL(recorded once, when version history was introduced, for a workflow that was already published then).publishedByUserIdis the id of the dashboard user who published or rolled back;nullfor hires, seeds, the backfill and internal callers.graphHashis a sha256 of the graph with key order ignored: two versions with the same hash have identical graphs.- Pruning. The newest 100 versions of a workflow are kept, plus the current one and any version a waiting conversation still runs on. Older ones are deleted, so version numbers can have gaps, and reading a pruned version answers
NOT_FOUND. - Settings are not versioned. Workflow settings (
failureResponseandnotifyOnFailure) take effect whenupdateWorkflowsaves them and are not part of any version, so a rollback or restore leaves them as they are.
currentVersionId is null for a workflow that was never published, and for one whose last publish happened before version history existed. A workflow with no entries gets its first one at its next publish.
Which version a run uses
Section titled “Which version a run uses”- A new turn runs the version that is current when the turn starts. A rollback or publish takes effect for the next turn.
- A conversation paused at an
await_replyorapprovalnode continues on the version it paused on, even if you publish or roll back in the meantime. Its next fresh turn uses the new current version. - Finished runs keep the version they ran.
WorkflowRunDto.workflowVersionIdrecords it. It isnullfor draft tests, for runs from before version history existed, and when the published graph could not be matched to a recorded version.
Can a session choose its version?
Section titled “Can a session choose its version?”No. A session always runs the version that is current when each turn starts. Neither the dashboard calls (startSession, sendMessage) nor the integration calls (sdkStart, sdkSendMessage) take a version number or a workflow selector, so you cannot pin a version per session or per message.
- To run an older version for real traffic, make it current first: roll back to it, or publish it again. The next turn of every conversation then uses it. A conversation that is waiting at an
await_replyorapprovalnode finishes on the version it paused on, as described above. - To try an edit without publishing, send the message with
testDraft: trueonsendMessage. That turn runs the draft graph, not any published version. By default it runs inMOCKmode: nodes that act on the outside world return mock results, while LLM nodes still call the model so the prompts are really tested. Only the workspace owner can ask forLIVE.testDraftis a dashboard call; it is not available onsdkSendMessageor with an API key. - The draft test is per turn. The flag is not remembered by the session: send
testDraft: trueon every turn of the test, including the reply that resumes a conversation paused at anawait_replynode. A turn without it runs the published graph. A draft test creates a normal session and a normal workflow run (mocked: truein MOCK mode,workflowVersionIdnull), soworkflowRun,workflowRunsand the session events work as usual. Start the session withisTest: trueto keep it out of the session lists. - Customers keep the published version.
updateWorkflowonly saves the draft; the published graph changes only onpublishWorkflowor a rollback. So you can edit and test the draft while every other session keeps the last published version, and publish when you are happy. The draft test also works on an agent that is not active and on a workflow that was never published, as long as the agent points at the workflow. - To test an old version, copy it into the draft (this replaces the current draft, so keep a copy of unpublished edits first) and test the draft with
testDraft, or compare the two withworkflowVersionDifffirst. - To see which version a finished run used, read
workflowVersionIdon the run (it isnullfor draft tests).
List the history: workflowVersions
Section titled “List the history: workflowVersions”Newest first, summaries only (no graph). Relay forward pagination: first (default 20, at most 100) and after (a previous page’s pageInfo.endCursor, which walks to older versions). There is no totalCount.
query VersionHistory($workflowId: Int!, $after: String) { workflowVersions(workflowId: $workflowId, first: 20, after: $after) { pageInfo { hasNextPage endCursor } edges { node { id versionNumber isCurrent source restoredFromVersionNumber note publishedByUserId nodeCount edgeCount createdAt } } }}{ "workflowId": 42 }{ "data": { "workflowVersions": { "pageInfo": { "hasNextPage": false, "endCursor": "djox" }, "edges": [ { "node": { "id": 318, "versionNumber": 3, "isCurrent": true, "source": "ROLLBACK", "restoredFromVersionNumber": 1, "note": "Back to the v1 prompt while we fix the classifier", "publishedByUserId": 17, "nodeCount": 3, "edgeCount": 2, "createdAt": "2026-10-16T03:12:44.000Z" } }, { "node": { "id": 305, "versionNumber": 2, "isCurrent": false, "source": "PUBLISH", "restoredFromVersionNumber": null, "note": "Stricter classifier prompt", "publishedByUserId": 17, "nodeCount": 3, "edgeCount": 2, "createdAt": "2026-10-15T08:40:10.000Z" } }, { "node": { "id": 291, "versionNumber": 1, "isCurrent": false, "source": "PUBLISH", "restoredFromVersionNumber": null, "note": null, "publishedByUserId": 17, "nodeCount": 3, "edgeCount": 2, "createdAt": "2026-10-14T09:02:31.000Z" } } ] } }}A cursor that was not issued by this query answers BAD_REQUEST (Invalid \after` cursor). A workflow that is not in your workspace answers NOT_FOUND`.
Read one version: workflowVersion
Section titled “Read one version: workflowVersion”The full graph of one version, exactly as published, including every node config (prompts, URLs, header templates), plus its summary.
query OneVersion($workflowId: Int!, $versionNumber: Int!) { workflowVersion(workflowId: $workflowId, versionNumber: $versionNumber) { summary { versionNumber source isCurrent } nodes edges }}nodes is a JSON array of { id, type, label, config, position: { x, y } } and edges a JSON array of { id, source, target, label? }, the same shapes as the workflow’s own draft. NOT_FOUND when the workflow is not in your workspace or the version does not exist or was pruned.
Compare: workflowVersionDiff
Section titled “Compare: workflowVersionDiff”What changed from fromVersion to toVersion: added, removed and changed node and edge ids, and for each changed node the names of the keys that differ (label, position, type, and config keys as config.<key>, for example config.promptTemplate). Values are never returned, because a config can hold secrets; read both versions with workflowVersion to see them. Every list is sorted.
Omit toVersion (or send null) to compare against the current draft. With fromVersion set to the current version, the result is exactly what the next publishWorkflow would change.
query WhatWouldPublishChange($workflowId: Int!, $fromVersion: Int!) { workflowVersionDiff(workflowId: $workflowId, fromVersion: $fromVersion) { addedNodeIds removedNodeIds changedNodes { id type changedKeys } addedEdgeIds removedEdgeIds changedEdgeIds }}{ "workflowId": 42, "fromVersion": 3 }{ "data": { "workflowVersionDiff": { "addedNodeIds": [], "removedNodeIds": [], "changedNodes": [ { "id": "classify", "type": "llm", "changedKeys": ["config.promptTemplate", "label"] } ], "addedEdgeIds": [], "removedEdgeIds": [], "changedEdgeIds": [] } }}An edge counts as changed when its source, target or label differs. NOT_FOUND when the workflow or either version does not exist or was pruned.
Roll back: rollbackWorkflow
Section titled “Roll back: rollbackWorkflow”Publishes an earlier version’s graph again, as a new version (source: ROLLBACK, restoredFromVersionNumber set). History is never rewritten, and the draft is not changed.
mutation Rollback($workflowId: Int!, $versionNumber: Int!, $note: String) { rollbackWorkflow( workflowId: $workflowId versionNumber: $versionNumber note: $note ) { id currentVersionId isPublished }}{ "workflowId": 42, "versionNumber": 1, "note": "Back to the v1 prompt while we fix the classifier"}- Returns the workflow with its new
currentVersionId. The change applies from the next session turn; paused conversations keep their own version (see above). - The old graph is checked against today’s rules, as a publish would be: node types, node configs, outbound URL checks and your workspace’s allowed models. A version that no longer passes is refused with the same
BAD_REQUESTorBAD_USER_INPUTerrorpublishWorkflowgives, and nothing changes. - Rolling back to a version whose graph is the one already live is refused with
BAD_USER_INPUT(Version 2 has the same graph as the version published now; nothing to roll back.). noteis optional, at most 500 characters after trimming (a longer one isBAD_USER_INPUT); a blank note is stored as none.NOT_FOUNDwhen the workflow or the version does not exist in your workspace.
Copy an old version into the draft: restoreWorkflowVersionToDraft
Section titled “Copy an old version into the draft: restoreWorkflowVersionToDraft”Replaces the draft’s nodes and edges with a version’s graph, including any unpublished edits in the draft. Nothing is published and no version is created: edit the draft, then call publishWorkflow.
mutation RestoreToDraft($workflowId: Int!, $versionNumber: Int!) { restoreWorkflowVersionToDraft( workflowId: $workflowId versionNumber: $versionNumber ) { id nodes edges }}The graph is checked the way an updateWorkflow save is: the size limits (BAD_REQUEST) and every node config (BAD_USER_INPUT, the same message a save would give). NOT_FOUND when the workflow or version does not exist in your workspace.
Notes on publish
Section titled “Notes on publish”publishWorkflow takes an optional note (at most 500 characters after trimming) that is stored on the version the publish records, for example what changed:
mutation Publish($id: Int!, $note: String) { publishWorkflow(id: $id, note: $note) { id currentVersionId }}The note is ignored when the graph is unchanged, since no version is recorded then.
A safe edit-and-release loop
Section titled “A safe edit-and-release loop”- Edit the draft with
updateWorkflow. - Run
workflowDiagnosticson the draft. - Call
workflowVersionDiff(workflowId, fromVersion: <current versionNumber>)to see exactly what the publish will change. publishWorkflow(id, note: "...").- If the new version misbehaves,
rollbackWorkflowto the previousversionNumber. Your draft still holds the new edits, so you can fix them and publish again.
Full field tables: Workflows API, version history.