Start a run, fill a step, complete it, and see where it stops.
Assumes installation is done and npm run dev is running. Output
blocks show what a local file-storage install returns.
integrator-onboarding ships with the seed data. Six steps, starting with a form.
curl -s localhost:3000/api/process-templates | jq '.[].key'
curl -s -X POST localhost:3000/api/process \
-H "Content-Type: application/json" \
-d '{"templateKey":"integrator-onboarding"}'
processId: ed9a17c8-0fab-4272-bd51-f9a58849e097 status: running steps: ["input"]
The process is created, the template is snapshotted onto it, and the first step is
pushed. Note steps — that is the list of step instances reached so far, not the
template's steps.
Writes address the instance, not the template step key. They are different ids.
curl -s localhost:3000/api/process/$PID | jq '.steps[-1]'
id: c8f0caa3-b54c-4fe0-ada8-f840a401a462 ← use this stepKey: input ← not this
This matters because a loop visits the same stepKey more than once, each visit
getting its own id.
curl -s -X PUT localhost:3000/api/process/$PID/steps/$SID \
-H "Content-Type: application/json" \
-d '{"name":"Test Partner","contractAddress":"0x1234","contact":"@test","agent":"Spark"}'
200. Now look at what happened:
context.input: {"name":"Test Partner","contractAddress":"0x1234","contact":"@test","agent":"Spark"}
audit entries: 1
2026-07-27T00:53:09 user=dev-user step=input keys=['name','contractAddress','contact','agent']
Two things to notice:
context.input — keyed by step key, which is how later steps
reference them as ${input.name}.stepContextAudit entry recorded who wrote what, when. Every write appends one.
Nothing is overwritten in the audit trail.curl -s -X POST localhost:3000/api/process/$PID/steps/$SID/complete \
-H "Content-Type: application/json" -d '{}'
HTTP 403
{"error":"Step completion is not allowed for your account, or the submitted data
does not satisfy the completion rule for this step."}
Rejected even as a dev user with all 8 permissions. The step has:
permissions: [] ← anyone may act
mandatory fields: [] ← nothing marked mandatory
completeExpression: trim(input.name).length > 0 && trim(input.contractAddress).length > 0
&& trim(input.contactPlatform).length > 0 && trim(input.contact).length > 0
&& trim(input.agent).length > 0 && trim(input.wallet).length > 0
completeExpression is the gate here, not mandatory. The request above sent four of
the six fields it requires.
mandatory is a per-field UI affordance. completeExpression is a whole-step rule
evaluated server-side, so it can express conditions a per-field flag cannot. This
template marks nothing mandatory and enforces everything through the expression.
The error message does not distinguish "you lack permission" from "your data is incomplete" — both return the same 403.
curl -s -X PUT localhost:3000/api/process/$PID/steps/$SID \
-d '{"contactPlatform":"Telegram","wallet":"0xabcd"}' # 200
curl -s -X POST localhost:3000/api/process/$PID/steps/$SID/complete -d '{}' # 200
status: running steps reached: ["input", "notify"] parked on: notify type=slack_notify audit entries: 2
The run advanced. steps now has two instances.
It is sitting on notify, a slack_notify step — and it will sit there forever.
npm run dev does not start the worker. Only input steps advance from an API
call; every other type needs the worker to come along and execute it.
Restart with:
npm run dev:all
Now the worker picks it up on the next tick. Without SLACK_BOT_TOKEN configured the
step will fail — but note how it fails: slack_notify records
the failure into context and advances anyway. The run continues — but the step is
marked as not sent, and the completion screen says so.
That is the soft-failure contract. See step types.
| Concept | Where it showed up |
|---|---|
| Template snapshot | process.template copied at start |
| Step instances vs step keys | steps[-1].id ≠ stepKey |
| Context keyed by step | context.input.name |
| Append-only audit | one entry per write, with actor and timestamp |
| Completion gating | completeExpression, not mandatory |
| Human vs automated steps | parked on notify until the worker ran |
With file storage, all of it is on disk in readable JSON:
cat .process-platform/process-states.json | jq # runs cat .process-platform/templates.json | jq # templates
Delete process-states.json to throw away every run and keep the templates. Delete the
whole .process-platform/ directory to reset both — the next template list/get gap-fills
shipped templates that are missing from the store. See
manage templates.