Recipes for agents that share things

Each recipe is one file you copy into your project: Node 18+ with no packages, and Python for some. They all use the same two calls, claim and release, so every decision gets a signed receipt you can check at verify.html. Bring any model and any agent framework.

Using an AI coding tool? Give it cascadedynamics.ai/SKILL.md and ask which recipe fits your project. Connecting Claude, ChatGPT, Codex, Cursor, VS Code or Grok? One URL does it all.

One name per worker.A claim from the same agent name counts as a renewal. Use something like host:pid.
Name each shared thing once.Every system that touches a resource must use the same cell name.
Short TTLs, renewed while working.If a worker dies, its hold frees after one TTL. Renewals are metered.
Start here

Check your integration, then check your receipts

Problem: you need to know Cascade is wired up right before you rely on it, and later you need to prove what happened to someone who doesn't trust you: an auditor, a customer, a regulator.

Self-test: on your own key, two agents race for one cell (exactly one wins and the other is told who), fences grow, a multi-claim is all-or-nothing, a waiting claim is granted when the cell frees up, and an outcome is signed. Then the whole record is verified offline against our published key. About 12 decisions.

Audit: point it at any saved record. It verifies it, prints who held each cell and how it ended, writes a CSV, and with a list of expected cells reports every one with no signed "done", for example an asset nobody inspected.

Tested: 9 of 9 checks on production on 2026-10-20, verified against the published key. Locally, an audit caught a coverage gap and a record where one holder's name had been changed.
# is my integration right?
export CASCADE_API_KEY=csc_...
node check.mjs
#   PASS  one winner in a race  ...
#   PASS  record verifies offline (cascade-verify, pinned key)
#   9 passed, 0 failed.

# what happened, and did everything get done?
curl -H "Authorization: Bearer $CASCADE_API_KEY" \
  https://api.cascadedynamics.ai/v1/runs/current/bundle > record.json
node check.mjs audit record.json --expect assets.txt --csv ledger.csv
#   coverage: 211/212 expected cells have a signed "done" outcome
#   MISSING  inspect/turbine-17
Swarms

Point a swarm of agents at a goal

Problem: you want several agents to work out the path to a goal together, without two of them doing the same task or trampling the shared plan.

Each agent picks an open task, does it, and adds the new tasks it discovered. One agent at a time edits the plan, and one checks the goal when nothing is left open. If an agent crashes, its task frees up for another.

Doesn't do: the thinking. Your model or code decides what each task means and when the goal is met.

Agents on different machines: pass board: { api: true, id: "my-goal" } and the plan lives in Cascade's hosted board instead of a file. Every plan change is signed into the record, and the server itself makes sure only the agent holding a task can mark it done.

Tested: 6 agents, 40-task goal, one agent killed mid-task each run. 5 of 5 runs reached the goal, each task done once, 0 double holds.
import { swarmAgent } from "./swarm.mjs";

await swarmAgent({
  agent: "agent-" + process.pid,
  board: "board.json",            // { goal, done, tasks: [...] }
  async work(task, board) {       // your model does the task
    const a = await askYourModel(board.goal, task.title);
    return { result: a.summary, next: a.followUps };
  },
  async checkGoal(board) {        // runs when nothing is open
    return { done: await runTheTests(), next: [] };
  },
});
Coding agents

Stop parallel coding agents overwriting each other

Problem: you run several Claude Code, Codex or Cursor agents on one repo, two open the same file, and the second write silently wipes the first.

A small hook asks Cascade for the file before every edit. If another session, or another subagent in the same Claude Code session, has it, the edit is refused and the agent is told who has it, so it moves on. Each session gives its files back when it finishes its turn, and each subagent when it finishes.

Doesn't do: guard files written from the shell (sed, scripts), or tell apart Codex subagents (Codex sends their edits under the parent session). If Cascade can't be reached, edits go ahead with a warning; set CASCADE_LOCK_FAIL_CLOSED=1 to refuse them instead, along with anything the hook can't check.

Tested with Claude Code's, Codex's and Cursor's documented hook formats: 8 test groups, including parallel subagents in one session, a crashed session, the API down, slow or answering nonsense, and malformed hook input. Subagent identity also checked live with Claude Code on 2026-10-24.
# 1. save the hook into your repo
mkdir -p .claude/hooks
curl -o .claude/hooks/cascade-file-lock.mjs https://cascadedynamics.ai/cascade-file-lock.mjs

# 2. .claude/settings.json
{ "hooks": {
  "PreToolUse": [{ "matcher": "Edit|Write|MultiEdit|NotebookEdit",
    "hooks": [{ "type": "command", "command": "node",
      "args": ["${CLAUDE_PROJECT_DIR}/.claude/hooks/cascade-file-lock.mjs"], "timeout": 10 }] }],
  "Stop": [{ "hooks": [{ "type": "command", "command": "node",
      "args": ["${CLAUDE_PROJECT_DIR}/.claude/hooks/cascade-file-lock.mjs"], "timeout": 10 }] }],
  "SubagentStop": [{ "hooks": [{ "type": "command", "command": "node",
      "args": ["${CLAUDE_PROJECT_DIR}/.claude/hooks/cascade-file-lock.mjs"], "timeout": 10 }] }],
  "SessionEnd": [{ "hooks": [{ "type": "command", "command": "node",
      "args": ["${CLAUDE_PROJECT_DIR}/.claude/hooks/cascade-file-lock.mjs"], "timeout": 5 }] }]
} }

# 3. export CASCADE_API_KEY=csc_...   then start your agents as usual
Actions

Never twice: payments, emails, deploys

Problem: an action must not happen twice, even when several workers race for it or one dies halfway.

Only the worker holding once/<key> may act. Workers that waited check whether it already happened before acting. Pass the same key downstream as the idempotency key, and keep the signed receipt with the outcome.

Doesn't do: on its own, stop a retry after a worker finishes and dies before saving. The downstream idempotency key closes that gap; always pass it.

Tested: 8 workers (Node and Python) racing, plus a worker killed mid-action. The payment ran once every time.
import { onceOnly } from "./once.mjs";

const r = await onceOnly(`charge/${order.id}`, `worker-${process.pid}`,
  (key) => payments.charge({ amount: order.total, idempotencyKey: key }),
  {
    alreadyDone: async (key) => (await db.get("outcomes", key)) != null,
    record: (key, outcome) => db.put("outcomes", key, outcome),
  });
Deploys

One deploy at a time, from CI, agents and people

Problem: a deploy, database migration or terraform apply can be started from CI, by an AI agent, or by a person at a terminal, and two at once break production.

Wrap the command. It waits for deploy/<service>/<env>, runs the command while renewing the lock, and signs done or failed into the record when it ends. The command gets CASCADE_FENCE to pass downstream. If the lock is ever lost mid-run, the command is stopped.

Doesn't do: roll anything back. Your deploy script still owns that.

Tested: three deploys started at once ran one after another, never overlapping, each with a larger fence. A busy lock with --wait 0 exits 75; a lost lock stops a 60-second command in under 10 seconds; exit codes pass through and failures are signed.
# any shell, any CI: GitHub Actions, GitLab, Jenkins, an agent's tool
node lock-run.mjs deploy/payments-api/prod --wait 600 \
  --agent "ci-$GITHUB_RUN_ID" -- ./deploy.sh prod

node lock-run.mjs migrate/orders-db -- npm run migrate

# held?  [lock-run] waiting: deploy/payments-api/prod is held by claude-agent-3
# done:  [lock-run] released deploy/payments-api/prod: done, receipt 665b5b85…
Data

Refuse late writes from an agent that lost its turn

Problem: an agent stalls (a long model call, a GC pause, a dropped network) past its TTL, someone else takes over, and then the first agent wakes up and overwrites the newer work.

Every grant carries a fence number that only grows. Send it with each write. Your store keeps the highest fence it has seen per cell and refuses anything lower, in one atomic step. Ready-made checks for Postgres (one SQL statement), S3 (conditional PUT) and Redis (one Lua script).

Doesn't do: protect writes that skip the check. Have a new holder raise the fence straight after its grant, so a stale write can't slip in before its first real one.

Tested: a holder that stalled past its TTL had its late write refused by the Postgres statement (run on SQLite), an S3-style conditional PUT against a local stand-in, and the in-process check, including across a Python and a Node process sharing one database file. Not yet run against real Postgres, S3 or Redis; the Redis script has not been executed yet.
import { claimFenced, pgFencedWrite } from "./fenced-writes.mjs";

const c = await claimFenced("doc/plan-7", "agent-a", 30000);
if (c.held) return;                                // c.behind has it
// one transaction: raise the fence, then write; a lower fence throws StaleFence
await pgFencedWrite(pg, "doc/plan-7", c.fence, (tx) =>
  tx.query("UPDATE plans SET body = $1 WHERE id = 7", [body]));

// S3: s3FencedPut({ head, put }, key, c.fence, body)
// Redis: redisFencedSet(evalFn, key, c.fence, value)
Cost control

A shared limit on expensive calls

Problem: cap how many LLM calls, GPU jobs or rate-limited API calls run at once, across every machine.

The pool is n cells. A call runs only while its caller holds one, so at most n run at once. If a caller dies, its slot comes back after one TTL.

Cost: a busy pool spends decisions on holds, because each waiting caller pays one hold each time a slot changes hands. Fewer waiters or slower polling cost less.

Tested: 9 processes, 36 jobs, a pool of 3, one holder killed. Never more than 3 in flight, measured at the work itself.
import { withSlot } from "./slots.mjs";

// at most 4 model calls in flight across every worker on this key
const answer = await withSlot("llm-calls", 4, `worker-${process.pid}`,
  (signal) => llm.complete({ prompt, signal }),
  { ttl_ms: 30000, pollMs: 200 });
Jobs

One copy only: cron jobs, workers, schedulers

Problem: a job must run as exactly one copy even though you start it on every machine.

One copy holds job/<name> and renews it while it runs. Every other copy is told who is running and exits. If the running copy dies, the next one you start after one TTL takes over.

Doesn't do: stop a frozen copy that wakes after losing its hold from finishing a step. Check the abort signal between steps.

Tested: three copies started, the running one killed. A new copy took over about 1.6 s after the kill with a 1.5 s TTL; never two at once.
import { singleton } from "./singleton.mjs";

const r = await singleton("nightly-invoices", undefined, async (signal) => {
  for (const batch of await pendingBatches()) {
    if (signal.aborted) return;      // lost the hold: stop
    await sendInvoices(batch);
  }
}, { ttl_ms: 15000 });
if (!r.ran) console.log("already running:", r.behind);
Computer use

Take turns on a login, browser profile or device

Problem: several computer-use agents share one admin login, browser profile or test phone, and two at once trample each other.

One agent at a time drives the account or device; the others wait for free. If the driver dies, the next agent gets its turn after one TTL.

Doesn't do: clean up after a crash. Start each turn by getting back to a known state.

Tested in one process: 4 agents and a dead holder, never two sessions at once.
import { withTurn } from "./turns.mjs";

await withTurn("account/acme-admin", "agent-7", async (signal) => {
  const browser = await openProfile("acme-admin");
  try { await agent.run("Rotate the API token", { browser, signal }); }
  finally { await browser.close(); }
}, { ttl_ms: 60000 });
People

Agents and people on one queue

Problem: agents and people pull from the same tickets, and a stuck or dead agent must not block a person from taking over.

Only the holder of ticket/<id> works it. A person who tries while an agent holds it is told who has it, and can take it when the agent lets go. The signed record lists everyone who handled each ticket, in order.

Doesn't do: store ticket status. That stays in your tool.

Tested in one process: 6 tickets, one agent dead mid-ticket, a person taking over two of them.
import { workQueue, takeTicket, whoHandled } from "./handoff.mjs";

await workQueue({ who: `agent-${process.pid}`, ttl_ms: 120000,
  openTickets: () => db.ids({ status: "open" }),
  work: async (id) => (await agent.solve(id)) ? { status: "resolved" } : { status: "needs-human" },
  save: (id, outcome) => db.update(id, outcome) });

whoHandled(bundle, "T-3");   // ["agent-412", "human:dana"]
Research

Crawl or research without repeats

Problem: research agents follow the same links, and each page should be fetched once, not once per agent.

URLs are normalized (case, fragments, tracking parameters), then claimed. While one agent fetches a page, the others skip it, and for a window after a good fetch too. It slots straight into the swarm loop above.

Doesn't do: remember forever. For a permanent "never again", keep your own list of fetched pages.

Tested in one process: 4 crawlers on a 30-page site with messy links and a dead crawler. Every page fetched exactly once.
import { fetchOnce, normalizeUrl } from "./crawl.mjs";

const r = await fetchOnce(url, agent, async (u) => (await fetch(u)).text(),
  { work_ttl_ms: 60000, keep_ms: 24 * 3600 * 1000 });
if (r.fetched) await save(normalizeUrl(url), r.result);
Robots, drones, vehicles

Reserve a route or a time window, all or nothing

Problem: robots, drones, humanoids or vehicles share aisles, airspace, bays, workcells or ground-station passes, and two of them must never plan through the same place at the same time.

Name the space as cells (a grid tile, an airspace sector at an altitude band, a 5-minute slot on a station) and reserve the whole route at once. Routes of any length are taken in one fixed order, so two robots on crossing routes can never deadlock. If a cell is taken, everything is handed back and you're told which cell and who has it, so the robot can re-plan. Free the cells behind you as you move.

Everything one job needs, in one decision: for a set of up to 16 things (a technician, a rack and its PDU; a dock, a crane and a lane; eight GPUs), withAll in reserve-all takes them in a single call, renews them together, and stops the job if any of them is lost. Taking them one at a time deadlocks; this can't.

Doesn't do: plan the route or control the robot. Your planner and autonomy stack do that; Cascade decides who may use the space. It doesn't queue fairly either: a job needing many cells can wait behind a stream of small ones.

Tested: 8 robots on heavily crossing 24-cell routes, half driving in reverse, 3 trips each and waiting for each other. 24 of 24 trips, no cell ever shared, no deadlock. A 40-cell route blocked at cell 34 handed back the cells it had taken. Two loaders taking a dock and a crane one at a time in opposite order deadlocked; as one-call reservations both finished in 0.65 s. 6 Node and Python processes ran 24 technician+rack+PDU jobs with one holder killed: 24 of 24 done, no resource ever used twice at once.
import { reserve, gridCells, airCells, timeCells } from "./reserve.mjs";

const r = await reserve(gridCells("dc-east", path), "amr-07",
  { ttl_ms: 20000, wait_ms: 5000 });
if (!r.ok) return replan({ avoid: r.cell });  // r.behind: who has it
r.keepAlive(() => robot.stop());            // renew while moving
await r.releaseBehind(passed);               // free the aisle behind you
await r.release({ status: "done" });

airCells("bay-area", sectors, "60-90m");           // drone corridors
timeCells("gs-svalbard", "2026-10-21T10:05Z", 12); // a satellite pass

// a small set, all at once, renewed together:
import { withAll } from "./reserve-all.mjs";
await withAll(["tech/ana", "rack/R12", "pdu/R12-B"], "planner-1",
  (signal, { fences }) => runMaintenance({ signal, fence: fences["rack/R12"] }));
Robots, humanoids, drones

Work in a zone only while you hold it, and stop on your own if you can't tell

Problem: a robot or drone has been given an aisle, a rack row or an airspace block, its network drops, and nobody can tell whether it is still working there when the next machine is sent in.

The robot renews its hold every third of the TTL. After each renewal it knows, on its own clock, the latest moment the hold is certainly still its own. If no renewal lands by then, it stops by itself, before Cascade can give the zone to anyone else, with no network needed. Every command carries the fence, so the controller refuses a command from a robot that has already been replaced.

Not a safety system. Cascade is a coordination layer and is not safety-rated. Round trips are about 150 ms at the median, so use it to decide who works where for the next seconds or minutes, never for motion control, collision avoidance or emergency stop. Those stay on the machine.

Cost: renewals are metered, 3 decisions per TTL per machine. A 6-second TTL is about 43,000 decisions a day per robot running full time; a 30-second TTL is about 8,600. For fleets, talk to us: info@cascadedynamics.ai.

Tested with Node and Python robots: when the link to Cascade was cut, the robot stopped about 1.05 s later, inside its 1.125 s budget and about 0.35 s before the next robot was given the zone. A command sent late was refused by fence. A robot killed outright was replaced after one TTL. An API redeploy mid-task didn't interrupt the robot.
import { acquireLease } from "./lease-safe-actuation.mjs";

const lease = await acquireLease("zone/aisle-7", "amr-07", {
  ttl_ms: 6000, margin_ms: 1500, wait_ms: 8000,
  onStop: (why) => robot.holdPosition(),        // local, no network needed
});
if (!lease.ok) return reassign(lease.behind);
while (taskLeft() && lease.valid())
  await lease.command((fence) => fleet.send({ zone: "zone/aisle-7", fence, step: next() }));
await lease.release({ status: "done" });         // only once out of the zone
Inspection, audit

Sign what was produced, then match any file to its receipt

Problem: agents or drones produce evidence (inspection photos, test reports, signed PDFs), and later someone needs to know that this exact file came from that job, by that agent, and that every asset was covered exactly once.

When the work is done, the release carries status and the file's SHA-256, signed into the same record as the grant. Later, hash any file and find its receipt, list every asset with no signed result, and check the whole record offline. It pairs with check.mjs audit, which turns the same record into a ledger an auditor can read.

Doesn't do: prove the file is right, only that it is the one that was reported. Cascade stores the hash, not the file: keep the file.

Tested: 3 Node and Python inspectors covered 12 assets with one killed mid-inspection. Every asset ended with exactly one signed result, every photo matched its receipt, a copy with one bit changed matched nothing, editing a signed hash failed verification, and cascade-verify passed.
import { withProof, fetchBundle, matchFile, coverage } from "./proof-of-work.mjs";

await withProof("asset/turbine-17", "drone-04", async (fence) => ({
  artifact: await camera.capture("turbine-17"),   // a path or a Buffer
  status: "done",
}), { alreadyDone, record });

const bundle = await fetchBundle("record.json");
await matchFile(bundle, "photos/turbine-17.jpg");  // [{ cell, agent, status, grant_hash, ... }]
coverage(bundle, assetCells).missing;               // assets with no signed "done"
Install instead of copying: npx -y cascade-coordination-mcp gives any MCP client (Claude, Cursor and others) claim and release tools with no code: claude mcp add --env CASCADE_API_KEY=csc_... --transport stdio cascade -- npx -y cascade-coordination-mcp. SDKs: pip install cascade-coordinate and npm install @cascade_dynamics/coordinate. Check receipts from a terminal with npx cascade-verify bundle.json (source). The MCP server also has board tools, and npx cascade-swarm "make the tests pass" --agents 4 --check "npm test" runs a swarm of Claude Code or Codex workers in separate git worktrees. In the API: a hosted shared board for plans, fencing tokens that reject late writes, signed outcomes on release, claiming several things at once (all or none), waiting for a release instead of polling, holds that survive restarts, and full-strength 256-bit receipts with a signed seal on new keys. See SKILL.md for the request shapes.

Tests ran on our machine against a local copy of the API on 2026-10-19 and 2026-10-20 (the check recipe also ran against production). They show the recipes behave as described. They are not production capacity figures.