Orch8: validate a bounded durable ReAct loop before model or tool calls
Pin and structurally validate Orch8's durable ReAct sequence before deployment, so an unbounded loop, broken route, embedded endpoint, credential field, or unresolved block reference fails in CI before any model or tool can run.
Run this workflow
CI-verified, 2/2 fixtures passing.
Build this with your agent
One copy-paste hands Claude Code, Codex, or Cursor the full recipe, steps included, nothing to fetch.
Intended Use
Teams evaluating or vendoring Orch8's ReAct example who want an inert, keyless CI gate over the durable control-flow definition before they deploy the sequence and give it an LLM key or a tool dispatcher. FlowStacks fetches one immutable JSON file, verifies its SHA-256, parses it locally, and checks the full bounded graph without installing or executing Orch8.
Not for
- Claiming the agent is safe or correct, the LLM chooses action names and arguments and the tool endpoint executes them; enforce an allowlist, authentication, least privilege, timeouts, and SSRF controls at that boundary
- Proving crash recovery or state persistence works in a live Orch8 engine, this validates the checked-in sequence definition as data and never starts the runtime or a database
- Production multi-tenant deployment without an independent threat review, Orch8's own historical audit still lists tenant-scoped scheduling, DNS-rebinding, expression-depth, and publisher-HTTPS design items
- Treating Orch8 as OSI open source, BUSL-1.1 restricts hosted or embedded uses that compete with the licensor until each version converts to Apache-2.0 after four years
The Stack
Tested Against
orch8-io/engine@4895153df3754ac64cef063f789a71632e8ee30creact-loop.json@sha256:db156db2b7ac03d882d2f8407eb65034e2853d1b42c5f0631f7a21b7648548b7node@20Side effects & data flow
- Network
- raw.githubusercontent.com
- Writes
- ./react-loop.json
- Credentials
- none required
Data privacy
- GitHub ← HTTPS request for one public, commit-pinned Orch8 JSON file; no workflow input, model prompt, tool data, or credential (retention: per GitHub's privacy statement)
Prerequisites
- Node.js 20+
- curl with HTTPS/TLS 1.2 support
Steps
- 1
Fetch the pinned sequence and prove its bounded control-flow graph
Download only the reviewed JSON artifact, not the Orch8 installer or binary. The validator rejects a digest mismatch, oversized or malformed input, duplicate/unresolved block IDs, any change to the ten-iteration observe/decide/act spine, a literal tool URL, or an embedded credential field. No model, tool endpoint, engine, or database is contacted.
curl --proto '=https' --tlsv1.2 --retry 3 --max-filesize 65536 -fsSL \ https://raw.githubusercontent.com/orch8-io/engine/4895153df3754ac64cef063f789a71632e8ee30c/docs/agent-patterns/react-loop.json \ -o react-loop.json node <<'NODE' const crypto = require("crypto"); const fs = require("fs"); function bad(message) { console.error("BAD: " + message); process.exit(1); } const raw = fs.readFileSync("react-loop.json"); if (raw.length > 65536) bad("sequence exceeds 64 KiB review limit"); const digest = crypto.createHash("sha256").update(raw).digest("hex"); const expectedDigest = "db156db2b7ac03d882d2f8407eb65034e2853d1b42c5f0631f7a21b7648548b7"; if (digest !== expectedDigest) bad("pinned source digest changed"); let doc; try { doc = JSON.parse(raw.toString("utf8")); } catch { bad("sequence is not valid JSON"); } if (!Array.isArray(doc.blocks) || doc.blocks.length !== 1) bad("expected one top-level block"); const all = []; function walk(blocks) { if (!Array.isArray(blocks)) bad("block collection is not an array"); for (const block of blocks) { if (!block || typeof block !== "object" || typeof block.id !== "string") bad("block missing id"); all.push(block); if (block.type === "loop") walk(block.body); if (block.type === "router") { for (const route of block.routes || []) walk(route.blocks); walk(block.default || []); } } } walk(doc.blocks); const ids = new Set(all.map((block) => block.id)); if (ids.size !== all.length) bad("duplicate block id"); const serialized = JSON.stringify(doc); for (const match of serialized.matchAll(/outputs\.([A-Za-z0-9_-]+)/g)) { if (!ids.has(match[1])) bad("unresolved output block id " + match[1]); } if (/https?:\/\//i.test(serialized)) bad("literal network endpoint embedded in sequence"); if (/api[_-]?key|authorization|bearer|password|secret/i.test(serialized)) bad("credential field embedded in sequence"); const loop = doc.blocks[0]; if (loop.type !== "loop" || loop.id !== "react_cycle") bad("root must be react_cycle loop"); if (loop.max_iterations !== 10) bad("loop must be capped at 10 iterations"); if (loop.condition !== "context.data.agent_done != true") bad("loop stop condition changed"); if (!Array.isArray(loop.body) || loop.body.length !== 2) bad("loop body must contain observe then decide"); const [observe, decide] = loop.body; if (observe.id !== "observe" || observe.type !== "step" || observe.handler !== "llm_call") bad("observe llm_call missing"); if (decide.id !== "decide" || decide.type !== "router") bad("decide router missing"); if (!Array.isArray(decide.routes) || decide.routes.length !== 1) bad("expected one finish route"); const finish = decide.routes[0]; if (finish.condition !== "outputs.observe.content.action == finish") bad("finish condition changed"); if (!Array.isArray(finish.blocks) || finish.blocks.map((b) => b.id).join(",") !== "finalize,mark_done") bad("finish route changed"); const [finalize, markDone] = finish.blocks; if (finalize.handler !== "set_state" || finalize.params?.key !== "final_answer" || finalize.params?.value !== "{{outputs.observe.content.final_answer}}") bad("final answer is not persisted"); if (markDone.handler !== "transform" || markDone.params?.expression !== "true" || markDone.params?.target !== "context.data.agent_done") bad("finish route does not stop loop"); if (!Array.isArray(decide.default) || decide.default.map((b) => b.id).join(",") !== "act,record_observation") bad("default route changed"); const [act, record] = decide.default; if (act.handler !== "tool_call") bad("act tool_call missing"); if (act.params?.tool_name !== "{{outputs.observe.content.action}}" || act.params?.arguments !== "{{outputs.observe.content.action_input}}" || act.params?.url !== "{{context.data.tool_dispatch_url}}") bad("tool dispatch bindings changed"); if (record.handler !== "transform" || record.params?.expression !== "outputs.act" || record.params?.target !== "context.data.observations") bad("tool observation is not recorded"); console.log("Orch8 ReAct spine OK: 1 bounded loop (10 max), observe -> decide -> act/record, finish persists answer and stops"); NODE - 2
Deploy and exercise the real agent only after the gate passes
Self-host or connect to Orch8, POST the reviewed sequence, then supply your model credential and a narrowly scoped tool dispatcher. Authenticate the dispatcher, allowlist tool names and destinations, reject private/internal network targets, and cap tool time and output size. Model decisions, external tool effects, and live recovery behavior are fenced and are not represented as CI-verified.
Eval, 2 fixtures
Last passed: verified todaybounded-react-spinecontainstimeout 60s · max $0Expected:
Orch8 ReAct spine OK: 1 bounded loop (10 max), observe -> decide -> act/record, finish persists answer and stopsclean-exitexit_codetimeout 60s · max $0Expected:
0
Results
The reviewed Orch8 sequence is a single loop capped at ten iterations. Each cycle calls an LLM observation step, routes a finish action to persisted final_answer plus agent_done=true, or dispatches the selected tool and records its observation before continuing. The gate verifies the exact pinned file digest, unique and resolvable block IDs, the finish/default routes, and that the tool endpoint remains a runtime context value rather than a hard-coded URL. This proves the durable workflow spine is reviewable and bounded; it does not validate model decisions, tool safety, recovery under a running engine, or Orch8's production security.
Did this work for you?
Our CI checks the setup runs. You tell us if the whole thing worked. Tell us straight.
Related workflows
- QM: prove the destructive-action denials hold in every security posture, even the loosest
- One shared memory for every coding agent: prove the configs actually point at the same server
- Validate a WrenAI semantic model's references before an agent queries through it
- Wire the DeepWiki MCP into your agent so it looks up repos instead of hallucinating
- Verify an agent-skills plugin before you ship or install it
- WebMCP: declare a site's agent tools, and gate the ones that spend money
Liked this workflow?
Get new verified workflows in WebAfterAI, three issues a week (Tue, Thu, Sat).