The Classic Workflow Engine
Flow Designer is what ServiceNow recommends and flow-designer is the skill for it. This is the other
engine — the one that still fulfils catalog items on most instances that have been live more than a few
years. blast-radius says outright that legacy workflows are not scanned, so an agent that cannot read
one is blind to a large part of what a record actually does.
Every tool here carries a ⚠️ LEGACY marker in its description. That is guidance about what to build,
not permission to ignore what exists.
The version model — read this before changing anything
This is the part that catches people out, and the tools do not hide it from you:
wf_workflow the workflow's identity. Name, table, description. Almost no content.
└── wf_workflow_version THE ACTUAL WORKFLOW. One per checkout. Has `published`.
├── wf_activity steps — belong to a VERSION, not to the workflow
└── wf_transition edges between activities — also per version
wf_context one running instance of a published version against one record
A workflow is not edited in place. Checking it out creates a new wf_workflow_version with
published = false; you edit that; publishing flips it to published = true and retires the previous
one. Records already running keep executing the version they started on — which is why a fix does not
apply to work in flight, and why wf_context rows can point at versions nobody can find in the UI.
So the first question about any classic workflow is which version:
await snow_query_table({
table: "wf_workflow_version",
query: "workflow.name=Standard Change Approval^published=true",
fields: "sys_id,name,published,sys_updated_on",
})
The trap in the write tools
snow_create_workflow_activity and snow_start_workflow both set the workflow_version field, and
both resolve a name by looking it up in wf_workflow — which returns a workflow sys_id, not a
version sys_id. wf_activity.workflow_version and wf_context.workflow_version reference
wf_workflow_version. Passing a name therefore points the new row at a version that does not exist.
Both parameters skip the lookup when you give them a 32-character hex sys_id, and that is the way to use them: resolve the version yourself and pass its sys_id.
const versions = await snow_query_table({ table: "wf_workflow_version",
query: "workflow=" + workflowSysId + "^published=true",
fields: "sys_id" })
const versionSysId = versions.records[0].sys_id
await snow_create_workflow_activity({ name: "Manager approval", workflowName: versionSysId,
activityType: "approval", order: 100 })
Even done correctly, adding an activity to a published version edits a live workflow underneath running contexts. The safe sequence is checkout → edit the unpublished version → publish, and checkout is a UI operation. If you cannot check out, do not write.
Reading one
snow_workflow_manage is the tool for everything except creation:
await snow_workflow_manage({ action: "list", table: "sc_req_item" }) // what runs on this table
await snow_workflow_manage({ action: "get", workflow_id: "Standard Change Approval" })
await snow_workflow_manage({ action: "get_history", context_id: ctxSysId })
get returns the workflow with its activities and transitions — enough to describe the graph without
opening the UI. list filtered by table is the fastest answer to "what else fires on this record", and
the one blast-radius cannot give you.
Running contexts
wf_context is one execution. It is where "the request is stuck" lives.
// everything currently executing for a workflow
await snow_query_table({ table: "wf_context",
query: "workflow_version.workflow.name=Standard Change Approval^state=executing",
fields: "id,state,started,workflow_version" })
await snow_workflow_manage({ action: "stop", context_id: ctx }) // cancels the execution
await snow_workflow_manage({ action: "retry", context_id: ctx }) // re-runs the failed activity
stop cancels the run and leaves the record where it is — approvals already generated are not withdrawn
and the record's state is not rolled back. Cancelling a context is not undoing a workflow; there is no
such operation. Clean up whatever the workflow created separately.
snow_workflow_analyze({ workflow_name, time_range_hours }) aggregates contexts over a window for error
and duration patterns. Use it before concluding a workflow is broken — "it never completes" is usually
one activity waiting on an approval nobody sees.
Starting one
await snow_start_workflow({ workflow_sys_id: versionSysId, table: "sc_req_item", record_sys_id: ritm })
It inserts a wf_context and returns. Workflows are asynchronous: a success here means the context was
created, not that anything ran. Poll wf_context.state rather than assuming.
Note the same version trap: pass a wf_workflow_version sys_id. If the call succeeds and nothing ever
runs, an unresolvable workflow_version is the first thing to check.
When to leave it alone
Classic workflows are a poor place to add behaviour and a fine place to read it. Prefer the honest answer when it applies:
- The change is new behaviour. Build it in Flow Designer, triggered on the same table, and leave the workflow untouched. Two engines on one record is normal on real instances.
- The workflow is not the actual problem. A catalog item that stalls is more often a missing approver or an inactive group than the graph.
- You cannot check out. Editing a published version under running contexts is not a change you can reason about.
The one case for editing it in place: the workflow is wrong and it must keep running for records that are mid-flight. Then it is checkout → edit the version → publish, and every context started before the publish keeps the old behaviour by design.
Related
flow-designer—sys_hub_*, the engine to build new automation in.blast-radius— does not scan classic workflows.snow_workflow_manage({ action: "list", table })is the manual step that fills that gap.update-set-workflow— a published workflow version is captured; a running context is not.