Update a workflow’s draft
PUT/
Changes a workflow’s draft. What runs live changes only when you publish.
Needs an API token with the Write permission for workflows (workflow:write).
Parameters
Path
idRequired · UUIDThe workflow’s ID.
Body
nameOptional · stringUp to 255 characters. Empty keeps the name; spaces alone are refused.
descriptionOptional · stringLeft out keeps it;
""clears it.definitionOptional · objectThe whole tree: the trigger, then each step through
nextAction. See Workflow definition.expectedRevOptional · integerThe
definitionRevyou read. If someone saved since, the update is refused with 409 instead of overwriting. Left out,null, 0 or below: no check.
What are the rarely used parameters?
tabOptional · stringOnly for the editor: its tab’s id. Leave it out.
testPayloadsOptional · objectThe saved test data. Left out keeps it.
pinnedDataOptional · objectPinned step output:
{"pins": {"step": {"data": …}}}. A pinned step replays its data instead of running, on test runs only; production always runs. Up to 2 MB a pin. Left out keeps it.triggerTypeOptional · stringLeave it out. GoRunner reads it from the trigger.
cronExpressionOptional · string or nullLeave it out. Nothing reads it: a schedule runs from its trigger’s
config.
What you send replaces what
A field you leave out stays as it is. definition replaces the whole tree: read the workflow, change the tree and send it all back.
Tags and the folder have their own calls: Set a workflow’s tags and Move a workflow to a folder. status changes only by publishing. Fields like these are ignored here, but one of the wrong type, such as "tags": "x", is a 400; sending back the whole workflow you read is fine.
A step without a pieceVersion is pinned to the app version it runs today, and a definition without schemaVersion is stored as sent and reads as 0: it is still a version 2 tree, and its export imports. The first update of a webhook workflow makes its webhookToken. A pin whose step changed is marked staleAt and no longer used. A pin whose step is removed, or now runs another app, is deleted.
Responses
- 200OK
The workflow after the change, with the next
definitionRev. idisn’t a UUID, the body isn’t JSON, or the name is over 255 characters or only spaces.The tree has a key it doesn’t take, no named trigger, two steps with one name, a new step name or display name over 255 characters, a
typeother than PIECE, CODE, BRANCH or LOOP, a branch step with no branches, or a new condition operator that doesn’t exist (the message lists the ones that do). Or a pin is over 2 MB.- 403
FORBIDDENThe token lacks the Write permission for workflows.
- 404
NOT_FOUNDNo workflow with this ID in the token’s workspace.
- 409
CONFLICTexpectedRevis out of date: someone saved since. Read the workflow again and redo your change. - 409
CONFLICTAnother save to this workflow took too long. Send the same request again.
Only with
tab: another editor tab holds the workflow. Nothing was saved.The workflow is live, and the new tree names an app, action or trigger this installation can’t run. The message names the step.
The tree adds a step on an app this workspace may not use. Steps already in the draft may stay.
Nothing was saved. Try again.
GoRunner couldn’t check which apps this workspace may use. Nothing changed. Try again.
Any call can also answer: 401 UNAUTHORIZED · 401 TOKEN_ENDED · 403 TOKEN_SURFACE_OFF · 403 FEATURE_OFF · 403 EMBED_NOT_ALLOWED · 403 EMBED_NOT_AVAILABLE · 403 ACCOUNT_SUSPENDED · 413 PAYLOAD_TOO_LARGE · 415 INVALID_CONTENT_TYPE · 429 RATE_LIMITED · 503 BUSY · 503 PAUSED · 503 SERVICE_UNAVAILABLE. See Errors.
Request
GORUNNER="https://api.gorunner.io"
GORUNNER_TOKEN="grt_…" # needs Write for workflows
WORKFLOW_ID="your-workflow-id"
curl -X PUT "$GORUNNER/api/workflows/$WORKFLOW_ID" \
-H "Authorization: Bearer $GORUNNER_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"name": "Order intake",
"expectedRev": 7,
"definition": {
"schemaVersion": 2,
"trigger": {
"name": "trigger",
"type": "webhook",
"pieceId": "webhook",
"actionId": "catch_webhook",
"config": {
"methods": [
"POST"
]
},
"valid": true
},
"settings": {
"timeoutSeconds": 120
}
}
}'const GORUNNER = "https://api.gorunner.io";
const TOKEN = process.env.GORUNNER_TOKEN;
const WORKFLOW_ID = process.env.WORKFLOW_ID;
const res = await fetch(
`${GORUNNER}/api/workflows/${WORKFLOW_ID}`,
{
method: "PUT",
headers: {
Authorization: `Bearer ${TOKEN}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
name: "Order intake",
expectedRev: 7,
definition: {
schemaVersion: 2,
trigger: {
name: "trigger",
type: "webhook",
pieceId: "webhook",
actionId: "catch_webhook",
config: {
methods: ["POST"],
},
valid: true,
},
settings: { timeoutSeconds: 120 },
},
}),
},
);
const { data } = await res.json();import os, requests
GORUNNER = "https://api.gorunner.io"
TOKEN = os.environ["GORUNNER_TOKEN"]
WORKFLOW_ID = os.environ["WORKFLOW_ID"]
res = requests.put(
f"{GORUNNER}/api/workflows/{WORKFLOW_ID}",
headers={"Authorization": f"Bearer {TOKEN}"},
json={
"name": "Order intake",
"expectedRev": 7,
"definition": {
"schemaVersion": 2,
"trigger": {
"name": "trigger",
"type": "webhook",
"pieceId": "webhook",
"actionId": "catch_webhook",
"config": {
"methods": ["POST"],
},
"valid": True,
},
"settings": {"timeoutSeconds": 120},
},
},
)
data = res.json()["data"]Response · 200 OK, shortened
{
"data": {
"id": "3b1e5c2a-7d4f-4e8b-a1c6-5f2d9b0e7a31",
"name": "Order intake",
"definition": {
"schemaVersion": 2,
"trigger": { … },
"settings": {
"timeoutSeconds": 120
}
},
"status": "active",
"definitionRev": 8,
"hasUnpublishedChanges": true,
"updatedAt": "2026-10-24T16:40:00Z"
}
}Every field
Body
nameOptional · stringTrimmed. An empty string is ignored (the existing name is kept), not applied; spaces alone are a 400.
descriptionOptional · stringSend
""to clear it; omit to keep it.definitionOptional · objectThe workflow definition, and the single most important shape in this API.
A workflow definition: its fields are in Workflow definition.
triggerTypeOptional · stringOverridden whenever the definition carries a trigger — it is re-derived from
definition.triggerafter the merge, so the value sent has no lasting effect.One of
manual,cron,webhook,callable,event,error,run_finished,poll.expectedRevOptional · integerThe
definitionRevyou read. When it no longer matches the stored row the save is refused with 409 instead of overwriting whoever saved in between. Omit it, or send null, 0 or a negative number, for an unconditional last-write-wins save.tabOptional · stringThe builder’s tab id (the opaque
tabits presence beat sends toPOST /api/workflows/{id}/presence). With it, the save is refused with 409EDITING_ELSEWHEREwhile the workflow’s advisory editing lock is held by another tab, the same person’s or someone else’s, so a tab that has not yet heard it lost the lock cannot write under the one that holds it. A free lock, or one this tab holds, lets the save through, and so does presence that cannot be read. Omit it (API clients, MCP and the partner API do): the editing lock is then not consulted, as before.testPayloadsOptional · objectThe canvas test-fixture library, stored outside the definition so publishing or restoring a version never resets it. Absent = keep.
pinnedDataOptional · objectThe pin library:
{"pins": {"<stepName>": {"data": {…}}}}. A pinned step emits its fixture INSTEAD of executing, and only onenvironment=testruns — production always executes for real. One pin is capped at 2 MB. Absent = keep. The server re-stamps the library on every save of the definition (this update, version restore, the assistant): a pin whose step config has changed is flaggedstaleAt, not deleted, and is never replayed; a pin whose step is removed, or now runs another app, is deleted with it, so a step that later takes that name starts with no pin.cronExpressionOptional · string or nullStored and answered as sent, and read by nothing: the schedule runs from the trigger’s own
config, and no write derives this. Leave it out.
Response · 200 OK
dataobjectA workflow row.
definitionis always the DRAFT — the tree the editor holds. What production actually runs is the snapshot named bylatestVersionId, which only changes when you publish.idUUIDnamestringdescriptionstringdefinitionobjectThe workflow definition, and the single most important shape in this API.
A workflow definition: its fields are in Workflow definition.
statusstringdraft= never published.active= live, triggers registered and the webhook URL reachable.paused= was live, now switched off. Server-owned: it moves only through publish/unpublish, never through a create or update body.One of
draft,active,paused.triggerTypestringMirrors
definition.trigger.typefor dispatch. Derived server-side from the definition by every update and publish, so treat it as read-only in practice. A create that sends its own value keeps it until then, unchecked.One of
manual,cron,webhook,callable,event,error,run_finished,poll.cronExpressionstring or nullNothing derives or reads this: the schedule runs from the trigger’s own
config, andnextRunAtprojects from it too. Null unless a caller wrote one withPUT /api/workflows/{id}.webhookTokenstring or nullThe secret segment of the public webhook URL. Made by the first update of a webhook workflow, or at publish; absent until then (so absent in the answers of create, duplicate and import). Returned ONLY to a caller holding
workflow:run: byGET /api/workflows/{id}and in the answers of update, publish, unpublish and restore (rotate answers it to its caller). It is never present on a list row, an export or a version snapshot. It is a bearer capability against an unauthenticated endpoint — possessing it is the ability to fire this workflow in production. Rotate it withPOST /api/workflows/{id}/webhook/rotate.latestVersionIdUUID or nullThe published snapshot production executes. Absent means nothing has ever been published.
definitionRevintegerOptimistic-lock revision, bumped by every definition save. Populated by
GET /api/workflows/{id}, by the update response and by the answers of create, duplicate and import; absent (0) on list rows. Send it back asexpectedRevon an update to refuse a clobbering write.hasUnpublishedChangesbooleanThe draft has moved ahead of the published version. Computed on the single-workflow read and on every list row; always
falsefor a workflow never published.folderIdUUID or nullAbsent/null = uncategorized. Changed only through
PUT /api/workflows/{id}/folder.tagsarray of stringsChanged only through
PUT /api/workflows/{id}/tagsor the bulkaddTagsoperation — never through the workflow update body.testPayloadsobject or nullThe saved test-fixture library, stored outside the definition so publish and version restore never reset it.
pinnedDataobject or nullThe pin library (
{"pins": {"<stepName>": {"data": …, "pinnedAt": …, "configHash": …, "staleAt": …, "pieceId": …}}}). A pinned step emits its fixture instead of executing, and only on test runs.configHash,staleAtandpieceIdare the server’s: a step whose config changed keeps its pin marked stale, and a stale pin is not replayed (the step runs); a step whose app was replaced loses it.workspaceIdUUIDStamped from the credential’s scope; never accepted from a request body.
orgIdUUIDcreatedByUUID or nullcreatedByNamestringList rows only: the creator’s name, else their email address. Absent when the creator is unknown or not a member of the organization (staff who built it on a support visit, someone since removed), and on every row for an embedded person whose installation switched
presenceoff (an embedded person is never told an address).createdAttimeupdatedAttimeWhen the workflow itself was last edited. Publishing, pausing, tagging and filing it are not edits and leave it alone.
failingbooleanList rows only: the workflow is live (
status: active) and its latest FINISHED production run (one that worked or failed) failed. A later run that is waiting, running, held or was cancelled changes nothing, and test runs never count. A paused or draft workflow is never failing.failingSincetimeOnly on a failing row: when the first production run failed after the last one that worked (runs cancelled, held or running in between neither break nor begin the streak). Left out for a caller without
run:read.failingRunobjectOnly on a failing row: the run that decided it, its latest finished production run, with why it failed. It is
lastRununless a later run is waiting, running, held or was cancelled. Withoutrun:readit carriesstatusandstartedAtalone.idUUIDstatusstringOne of
failed.startedAttimefinishedAttime or nullerrorobjectWhy it failed, as
lastRun.errorsays it.stepstring or nullstepKeystring or nullmessagestringsummaryobjectList rows only: what a list draws instead of the definition.
triggerobject or nullThe chosen trigger; null while none is chosen.
pieceIdstringactionIdstringdisplayNamestringpiecesarray of objectsThe apps the workflow uses, each once, in walk order (the trigger first), with their display names.
idstringnamestringstepCountintegerSteps after the trigger, across branch arms, loop bodies and error paths.
needsSetupintegerSteps (and a chosen trigger) the editor marks to configure:
valid: false, or no app and action chosen.runs7darray of integersList rows without
view=summaryonly: counted runs per UTC day over the last seven days, oldest first (production runs that are not a sub-flow’s, cancelled runs left out).nextRunAttimeNext fire time of an active schedule workflow, projected from its cron in the trigger’s own timezone. Populated by the list endpoint.
lastRunobject or nullThe most recent PRODUCTION run’s outcome (test runs never count). Populated on list rows only. Without
run:readit carriesstatusandstartedAtalone.idUUIDfinishedAttime or nullerrorobjectOnly when
statusisfailed: why.stepstring or nullThe display name of the step it failed at; null when the reason names no step.
stepKeystring or nullmessagestringThe reason’s first clause, at most 160 characters.
statusstringOne of
pending,running,completed,failed,cancelled,paused.startedAttime