Cognitive Fab › polyflow › Get started on Temporal
Get started · experimental

Polyflow for Temporal

Govern the agents you already run on Temporal. Polyflow installs as a worker plugin, and your workflow code stays byte-identical. There are four levels, each one change to your code, and you can stop at any of them.

git clone -b platform/temporal-governance https://github.com/cognitive-fab/polyflow
cd polyflow/platform && npm install
BUSL 1.1, in-house production use granted kernel and verifier Apache-2.0 Node ≥ 22.13 @temporalio/worker ≥ 1.24 Python temporalio ≥ 1.33, optional
01 / Install

A dev server and the test suite

Everything on this page runs against Temporal's local dev server. Run the tests once so you know your environment is complete; the Temporal suite downloads a dev server on first run.

Every check below is a consistency check, not a proof. It is exhaustive over the finite domains a contract declares, and nothing more.

temporal server start-dev
(cd packages/kernel && npm test)
(cd packages/temporal && node scripts/test-each.mjs)

On Windows, some sample paths are long: clone with git -c core.longpaths=true clone … if checkout reports "Filename too long".

02 / G0 Observe

Record every effect: one line

Add the plugin to a worker, last in the list: it must see the final input every other plugin produced. From then on, every activity, child workflow, signal and Nexus operation a workflow orders becomes a proposal, a verdict, an effect and an observation on a hash-chained ledger.

The events ride on the headers of the activities the workflow already schedules, so Temporal's history alone can rebuild the record. A sink that is down loses nothing.

plugins: [new PolyflowPlugin({ sink: fileSink('./ledger'), signingKey })]
npx polyflow verify ./ledger/default/<workflow-id>/<run-id>.jsonl --trust ./keys/trust.json
default/my-workflow/018f… — 14 events chain intact through seq 13 signatures 3 head(s), 3 trusted and anchored, signed through seq 13 closure present — the record is finished OK — consistent, closed and signed through its last event. A consistency check, not a proof.

The verifier is Apache-2.0, so an auditor or a security team can check a record with no licence conversation.

03 / G1 Guard

Rules checked before each effect

A policy names your activities as effects, each with a consequence class, and states rules over their sequence: requires-prior, at-most, budgets, rates, and approvals bound to one effect. Admission refuses a policy that guards something no effect declares.

A call that would break a rule fails before it is scheduled, with a witness the agent can re-plan from. Nothing in the agent's prompt needs to know the rules.

{ "effects": { "ask_approval": { "kind": "approval", "class": "none" }, "slack_send": { "kind": "post", "class": "irreversible" } }, "rules": [ { "id": "no-post-without-approval", "type": "requires-prior", "guards": "post", "prior": "approval" }, { "id": "at-most-one-post", "type": "at-most", "guards": "post", "n": 1 } ] }
npx polyflow policy policy.json --out admitted.json
// the activity call throws, before it is scheduled err.cause.type → 'PolyflowDenied' witness.rules[0].fix → "run 'approval' and wait for it to succeed before 'post'" witness.allowedNow → ['approval', 'fetch', 'read']

Then set level: 'guard' on the plugin. An activity the policy does not name is denied unless you say otherwise: a guard that allows what it does not know fails open.

04 / G2 Govern, G3 Certify

Temporal's own expense sample, as a certified machine

Upstream, "no payment without approval" is one if in the workflow, and the approval signal names nobody. In the port, the control flow is a small state machine that admission checks over every path, and activities.ts is byte-identical to Temporal's.

Delete the approval step and admission refuses the machine, naming the guarantee it breaks. With a trust store on the worker, a machine whose bytes differ from its certificate is refused at start.

cd examples/temporal_samples/expense
npm run server             # the sample's expense server on :3000
npm run start              # the worker
npm run workflow-approve
Done: { expenseState: 'completed', expenseId: 'my-business-id', reason: '' }
npx polyflow admit ./machine
✔ domain ✔ check-effects paths 9, states 12 ✔ structural every-state-can-finish:ok stop-from-every-state:ok … ADMITTED expense as cert-584e5a058b75 (UNSIGNED: workers will refuse it until it is signed) guarantees: at-most-one-payment-per-path, no-payment-without-prior-approve, payment-implies-approval-was-requested, no-payment-after-reject-or-timeout, … exhaustive over the declared domains only. A consistency check, not a proof.

In CI, --key ci.key.json signs the certificate; a worker with a trust store refuses anything unsigned.

05 / New versions

Move runs by state, not patched()

Temporal's answer to changing a workflow with runs in flight is patched(): both branches stay in the code until the old runs drain. With a certified machine the question becomes checkable: for every state a live run holds, can the new version take it?

vet answers per run: auto-upgrade, migrate, or pin. The gate promotes the new version under Worker Versioning, and a step in flight finishes where it started. The saga port adds a v2 this way, with no patched() in the code.

npx polyflow vet --old ./machines/v1 --new ./machines/v2 \
  --fleet fleet.json --trust ./keys/trust.json
cd examples/temporal_samples/saga
npm run start       # v1 worker
npm run workflow    # account opened
npm run start.v2    # v2 worker

A run that started under v1 gets v2's behaviour. That is a migration the operator accepts on vet's report, not an equivalence, and the record stays one chain across the hand-over.

06 / Python

An unmodified OpenAI Agents SDK agent, at G1

The Python plugin covers G0 and G1, and its kernel is pinned byte for byte to the TypeScript one by a shared conformance corpus. The SDK routes every MCP tool through one activity per server, so the policy classifies by the tool the call carries.

In Temporal's customer_service sample, a seat change before a lookup is refused with a witness. The agent looks the booking up, then changes the seat, once. The ledger verifies under the TypeScript CLI.

worker = Worker(client, task_queue="agents", workflows=[...], activities=[...], plugins=[PolyflowPlugin(level="guard", policy=admitted, sink=FileSink("./ledger"), signing_key=key)])

A LangGraph binding covers the same two levels without Temporal: it records and guards an unmodified LangGraph agent's tool calls with the same ledger and guard.

Next