An agent can already resume a parked run. It cannot tell you what the resumed run is allowed to do.
A workflow engine for AI agents. The agent reasons about a workflow instead of the next tool call; polyflow admits that workflow only if it model-checks, then runs it durably and hands the agent one work order at a time. It ships as an MCP server, holds no credentials, and calls no tool itself.
Today an unattended automation is approved by verb.
polyflow approves the plan.
“Allow slack_send to #cs” — forever, for whatever the model decides to do with it. That is the ceiling when the plan is a prose instruction re-planned on every run.
Most agents run a loop: look, think, call one tool, look again. The plan exists only in the transcript. When the transcript is compacted, summarised, or simply ends, the plan goes with it — and the next session has no way to know the last one happened.
That is not a model-quality problem. It is structural. A scheduler that re-fires a missed job starts a fresh session with a fresh context, and the job runs again from the top. Every side effect it already performed, it performs a second time.
The value is not “the model might misbehave.” It is that state which lives only in a conversation cannot survive the conversation ending.
The second problem is what an approval means. A standing grant names a tool and a target. It cannot express “post only after a human said yes,” because there is no artifact for that sentence to be true of. polyflow makes the workflow that artifact, and checks the sentence against every path through it before the workflow is allowed to load.
In an ordinary workflow runtime, the runtime executes the effect — so the runtime holds the credentials, the connectors and the permission engine. polyflow has none of the three. The agent has all three. So polyflow does not perform the effect; it hands it back as an order.
The run's identity is derived from validated input, not chosen by the caller. A nightly task resumes instead of restarting, and an agent cannot rename its way to a second run.
polyflow names the tool and the arguments. The agent runs it through its own gates and reports. Only then does the completion action dispatch and the next order appear.
The pending map is in memory, so a crash loses the promise, the lease expires, the effect is re-claimed and the order is re-offered — same intent id, at-least-once, absorbed by the machine.
Six MCP tools: workflow_list, workflow_start, workflow_report, workflow_state, workflow_signal, workflow_journal. The journal is also a valid Polygraph trace corpus.
A workflow ships its guarantees as code: small predicates over an emission path that must hold no matter which way a run goes. Here is one from the example workflow, which posts a daily customer brief to a Slack channel.
{ name: 'no-post-without-prior-approval',
pred: (path) => path.emitted.every((e, i) =>
e.kind !== 'post_brief' || path.actionBefore('APPROVED', i)) }
At startup polyflow enumerates every reachable emission path over the domain the contract declares and checks every rule against every one of them. A workflow that fails is refused:
[polyflow] admitted: customer-brief — paths explored: 5 · states seen: 10 · exhaustive within declared domains [polyflow] REFUSED: unsafe-brief [polyflow] no-post-without-prior-approval
unsafe-brief is the deliberately broken twin: one line moved, so it posts on entering review — before the human answers. It still calls ask_user, still targets the same channel, still satisfies the standing grant. A reviewer reading the diff could easily miss it. The gate does not.
This is the comparison worth making carefully, because on the surface polyflow and a durable-execution engine both “run a workflow that survives a restart.” The difference is entirely in how — and the how is what decides whether it works with a model in the loop.
Recovery re-executes the workflow code against a recorded event history and expects the same decisions to come back out. That is a determinism requirement on the workflow body, and a language model does not satisfy it. The standard escape — wrap every model call as an activity — puts the sequencing back in the model, which is the thing that was supposed to be taken out of it.
polyflow: the state after each step is a committed snapshot. There is nothing to replay, so non-determinism upstream is simply irrelevant. A second process reloads the snapshot and continues.
Temporal, LangGraph and Step Functions all constrain what a run does next — the code or the graph fixes the transitions. None of them takes a sentence like “no post without a prior approval” and establishes it over every path before the workflow is allowed to run. The graph is reviewed by eye.
polyflow: the guarantees are a file, model-checked at load. Failing one is not a warning — it is a refusal to register the workflow at all.
Which means the durable-execution engine holds the connectors and the secrets, and the agent's own permission model — the approval dialogs the user already trusts — sits somewhere else entirely, or nowhere.
polyflow: zero credentials, zero connectors, no permission engine. Every side effect goes out through the agent's own tools, under the user's existing gates. That is what makes it mountable inside an agent rather than beside one.
Deduplication depends on the caller passing the right one. An agent that sees “this run already finished” and would rather not stop can pass a different id — and in our own runs, one did exactly that.
polyflow: the workflow declares how its runs are named and polyflow derives the key from validated input. A supplied key that disagrees is ignored, with a note saying so.
There is a second family to place: systems that let the agent write its own procedures from past successes — Voyager, Agent Workflow Memory, Memp, WorkflowGen, CodeMem. They store a procedure as text or code the model must read and re-enact, so the procedure degrades exactly the way context degrades: under compaction, across sessions, at the mercy of what the model recalls. polyflow's procedure is an executable artifact the agent queries. A second session does not remember that the brief was posted. It asks, and is told.
| System | Who writes the procedure | Where it lives | Checked before it runs | Survives a restart | Limits what the run can do next |
|---|---|---|---|---|---|
| The agent writes its own procedures from past successes | |||||
| Voyager | the agent, after a task succeeds | code snippets pulled back into the prompt | ◐ | ○ | ○ |
| Agent Workflow Memory | the agent, from past runs | text in the agent's memory | ○ | ○ | ○ |
| Memp | the agent, summarising past runs | instructions and templates | ○ | ○ | ○ |
| WorkflowGen | the agent, from past runs | a reusable plan structure | ○ | ○ | ◐ |
| CodeMem | the agent, as code | code run through MCP tools | ○ | ◐ | ◐ |
| A developer writes the procedure ahead of time | |||||
| Temporal | a developer | workflow code plus a replayable event history | ○ | ● | ◐ |
| LangGraph | a developer | a graph, with an optional checkpoint store | ○ | ◐ | ● |
| AWS Step Functions | a developer | a state machine definition on the service | ◐ | ● | ● |
| The procedure is written in plain English | |||||
| OpenWorker scheduled jobs | the user, in English | an instructions string, re-planned on every run | ○ | ◐ | ○ |
| Kiro Crew scheduled jobs | the user, in English | a task description, re-planned on every run | ○ | ◐ | ○ |
| polyflow | the agent, as a state machine | a stored machine the agent queries | ● | ● | ● |
● yes · ◐ partly, or by a weaker mechanism · ○ no. Each row reflects what that project describes in its own documentation. The OpenWorker row was read from its source code.
Several projects now check something before an agent can use a tool, and it is worth being exact about what each one checks — because they are not substitutes for each other, and a team can reasonably want both.
The Hermes Agent publishes a catalog of remote MCP servers, and an entry has to pass an automated probe before it appears. The probe connects, follows the authentication challenge, and requires the server to support dynamic client registration — the standard way for a client to sign itself up without a human pasting credentials. Servers that do not are dropped.
The same pass trims tool bloat as a side effect, taking one connector from 262 tools down to 40.
Adjacent idea, human instead of probe: teams publish their MCP servers, agents and skills into a catalog, and an approver decides which records become visible. Both people and agents search it.
Records can pull their own metadata from an HTTPS endpoint, which a locally-started process has no way to offer — so polyflow publishes as a manual record.
Both answer may this server be connected to at all. Neither says anything about what it will do once an agent starts using it.
That is the line worth drawing. A registry check is about infrastructure hygiene — is the authentication sound, is the tool list sane, does the server behave. polyflow's check is about a different object entirely: on every path this workflow can take, does it break a rule someone wrote down.
A workflow can be perfectly well-behaved as infrastructure and still post to Slack before anyone approved it. The registry gates what may be found; polyflow gates what a run may do. They sit at different points in the same pipeline, and an organisation can use both.
OpenWorker is Andrew Ng's open-source desktop agent. polyflow was mounted over MCP with no change to OpenWorker itself: its own turn engine, tool registry, permission engine and provider stack, running headless. The model was DeepSeek V4 Flash. The agent was handed both surfaces — the six polyflow tools and four instrumented ordinary tools — and the task never mentioned workflows: “gather yesterday's support tickets, draft the customer brief, get it approved, post it to #cs.” Finding the workflow was part of what was being measured.
workflow_list → workflow_start → github_search_issues → workflow_report
→ draft_text → workflow_report → [ask_user] → workflow_report
→ slack_send → workflow_report (11 iterations, briefState: posted)
The case we expected to fail was the refusal. When the human says no, the agent has to report it as a permanent failure rather than a retryable error, or the run walks into posting anyway. It got that right every time, unprompted — final state denied, carrying the human's words, and slack_send never reachable. In one configuration the ask_user tool was unavailable and returned an error; the agent reported the failure, retried, then stopped and said the run was parked in review. The failure mode a prose instruction invites — “I couldn't ask, but the task says post it” — did not occur.
| Condition | Slack posts | Runs that posted twice | Tool calls (median) | Posts without approval |
|---|---|---|---|---|
| The same job ran twice in one day | ||||
| No polyflow | 2 in every run | 8 of 8 | 8.5 | 0 |
| polyflow | 1 in every run | 0 of 8 | 4.5 | 0 |
| The job ran once | ||||
| No polyflow, human approved | 1 in every run | 0 of 8 | 4.5 | 0 |
| polyflow, human approved | 1 in every run | 0 of 8 | 4 | 0 |
| No polyflow, human refused | 0 in every run | 0 of 8 | 5 | 0 |
| polyflow, human refused | 0 in every run | 0 of 8 | 3 | 0 |
OpenWorker's scheduler re-fires a job missed while the machine was off, and every fire is a fresh Run of the task's instructions — a new session with no memory of the last. Without polyflow the second session redid the whole job and posted again, in all eight runs. Nothing in the prose could have prevented it: the second session had no way to know the first existed. With polyflow, it asked for the workflow by name, saw it had already finished, and stopped.
npm install # pulls polygraph (polyrun) as a dependency npm test # 14 tests, no API key, deterministic node bin/polyflow-mcp.mjs # MCP stdio server
It is a plain MCP server started as a local process, so any agent that can act as an MCP client can use it — supporting a new host means writing that host's configuration file, not changing polyflow. The installer writes it for you:
| Host | Where the entry goes |
|---|---|
| OpenWorker | mcp.json in the global connector config — the same file the Connectors page edits |
| Kiro Crew and Kiro | ~/.kiro/settings/mcp.json, or a project's .kiro/settings/mcp.json, which wins when both name the same server |
| NVIDIA NeMo Agent Toolkit | its mcp_client function group — the installer prints the YAML block. NeMo can also run as an MCP server itself, so a NeMo workflow can be one of the tools a work order names |
| Claude Code, Cursor | a project's .mcp.json |
| AWS Agent Registry | a manual record, which makes polyflow discoverable inside an organisation |
Kiro Crew is worth calling out, because it runs recurring jobs unattended on a schedule — the same situation the results above measure: a job that fires a second time with no memory of the first.
More than one agent on a run — a crew sharing one broker, claiming orders, never doing one twice — is polycrew, which uses polyflow as a library and never forks its gate.
Node 22+. A workflow is six files in a directory — a contract, a SAM v2 strict-profile module, a pure effect mapper, a manifest, the guarantees, and the descriptor that says how runs are identified. Copy the example and edit it; a workflow that fails its check is refused at startup rather than at 3AM.