Technical documentation

AIOrc: a multi-agent workflow compiler over MCP

AIOrc compiles visual workflow graphs into a single deterministic markdown document that the target model executes sequentially via Model Context Protocol. Unlike orchestrators with an active runtime (LangGraph, CrewAI, AutoGen, Claude Agent SDK), AIOrc emits the full flow in a single MCP call — eliminating inter-agent latency and preserving context coherence within a single LLM session. A second, server-verified mode (workflow.start/workflow.next) trades that single round-trip for hard enforcement: the server dispatches one step at a time and validates every transition against the graph.

Summary and positioning

Multi-agent orchestration frameworks (LangGraph, CrewAI, AutoGen, Semantic Kernel) tackle the "how to coordinate several LLMs on one task" problem from the runtime side: they keep an active control loop, call the LLM for each agent, persist shared state, and decide the next step after each response. It's flexible, but it has three costs: latency accumulated per round-trip, context fragmentation between agents, and an orchestration runtime that lives in the target.

AIOrc proposes an alternative: if the flow graph is known a priori — with its branches, cycles, and parallel forks — then it can be compiled before execution. The product of the compilation is a single markdown document that describes agents, skills, topology, and traversal rules. The target LLM loads it once and traverses the graph within its own reasoning, in a single session. There is no active runtime. No round-trips. No shared state outside the LLM's own context.

Key finding

A 5-agent flow that in a traditional orchestrator requires 5 sequential LLM calls (with their respective network latencies, schema validation, and serialization) becomes, in AIOrc, a single MCP call that returns the compiled workflow — and the target model traverses the topology within its own session, where inter-step "latency" is token generation speed. When there are parallel nodes, the LLM can additionally fire several branches in the same response via concurrent Agent tool calls.

The trade-off is known and explicit: AIOrc does not support mid-run intervention, does not allow intermediate streaming between agents, and depends on the target LLM to interpret natural-language conditions and respect per-agent caps. We call this soft determinism — the flow is deterministic in structure (always the same compiled output for the same graph + input) but its traversal depends on the LLM's fidelity to the instructions.

AIOrc technical stack

AIOrc is deliberately minimalist. The underlying idea (compile before orchestrating) does not need a complex stack:

ComponentTechnologyRole
HTTP serverExpress 4 + TypeScriptMulti-tenant REST API + MCP endpoint at /mcp
DatabaseSQLite via better-sqlite3Synchronous, single-process. ~18 tables: users, projects, agents, skills, project_flows, runs, plus relation tables (agent_skills, project_agents, project_skills), stars (project/agent/skill), invitations (project/agent/skill), and the issues module
Authbcrypt + JWTUser sessions; project API keys authenticate MCP calls via x-project-key or x-project-id (the latter for public projects)
ValidationAjv (JSON Schema)Per-agent input schemas; field whitelist on routes
MCP transportstdio (Node.js bridge)mcp-bridge.js — script the Claude client launches as a subprocess, translating stdio ↔ HTTP
Flow editorReact 18 + @xyflow/react + ViteDrag-and-drop canvas in src-frontend/flow-editor/, bundle served from public/flow-editor.js
Remaining pagesVanilla HTML/CSS/JSdashboard, project, repository, user, issues, faq, index — no framework
Orchestrator~430 lines of TypeScriptsrc/orchestrator/index.ts — structural validator + markdown compiler

The core of the system fits in a few files:

No Redis, no queues, no workers, no containers. Single Node binary + single SQLite file + a static Vite bundle for the flow editor. The argument is: if the flow compiles in a few milliseconds and execution is done by the target LLM, we do not need orchestration infrastructure.

Why compile instead of orchestrate

Inter-agent latency

In a traditional orchestrator, the total latency of an N-agent flow is approximately:

total_latency ≈ Σᵢ (network_call_i + tokens_in_i / throughput + serialization_i + decision_i)

Where each summand includes the network round-trip to the LLM provider, token generation, and the orchestrator's decision about what to do next. For 5 agents with medium-sized responses, this is usually 8-30 seconds spent in orchestration alone.

In AIOrc, the latency is:

total_latency ≈ network_compilation + tokens_in_total / throughput

A single local MCP call (~1-5ms) and tokenized generation of the entire flow. No pauses between steps.

Context coherence

Multi-agent frameworks typically isolate each agent's context, passing only relevant subsets via tool calls or shared state. This is deliberate — it tries to avoid context bloat and delegate cleanly — but it introduces a cost: the model never has full visibility. A downstream agent doesn't know what the upstream reasoned about, only the literal output.

AIOrc injects the entire flow as a single markdown document into the target model's context. Each step sees the previous steps and the steps ahead. This enables internal consistency, cross-references ("as task-planner said"), and cross-cutting reasoning over the entire flow.

No agent runtime at the destination

The target project does not need to install LangChain, CrewAI, or any orchestration SDK. It only needs:

  1. An MCP client — Claude Code, Cursor, Continue, or any MCP-compatible IDE already has this.
  2. The AIOrc stdio bridge (mcp-bridge.js, ~50 lines).
  3. A rule in CLAUDE.md that tells the model to call workflow() before responding.

This reduces the blast radius: the destination remains a regular code project, not a project with an orchestration runtime woven into it.

How the compiler works

The core of the system is compileFlow(projectId, input) in src/orchestrator/index.ts. Unlike orchestrators with an active runtime, AIOrc does not execute the flow — it transcribes it into a markdown document that describes topology, agents, and rules so the target LLM executes it within its own reasoning. The graph has four node types and edges with natural-language conditions:

// src/db/schema.ts export type FlowNodeType = 'start' | 'agent' | 'parallel' | 'end'; interface FlowNode { id: string; type: FlowNodeType; agent_id?: string; // only for type='agent' agent_name?: string; // denormalized for display max_invocations?: number; // only for 'agent', default 10, cap 50 outcome?: string; // only for 'end', free-form label label?: string; // typically used by 'parallel' x: number; y: number; } interface FlowEdge { id: string; from: string; to: string; condition?: string; // natural language; absent = fallback priority?: number; // lower evaluates first; default 1000 }

There are no separate primitives for "decision", "gate", or "loop". That expressiveness emerges from combining edges with conditions, priorities, and back-edges:

Compilation algorithm

  1. Flow loading. Reads project_flows.flow_json and the referenced agents. If there is no flow or it is empty, returns no_flow/empty_flow error.
  2. Normalization (normalizeFlow). Applies defaults: priority=1000, max_invocations clamped to [1, 50], condition normalized to a non-empty string or undefined.
  3. Fail-closed validation (validateFlow). Five structural rules: exactly 1 Start (with 0 incoming, 1 outgoing), Ends with no outgoing edges, Agents with agent_id that exists in the project, edges with valid endpoints, Parallels with ≥1 incoming and ≥2 outgoing. If there are errors, returns invalid_flow with the concatenated messages — the MCP client receives them in the JSON-RPC error.
  4. Skill hoisting. Collects the unique skills referenced by all agents and emits them once at the top. Each agent references them by name, avoiding token duplication.
  5. Sub-agent emission. For each agent node, emits a block with name, max_invocations, description, expected_output_format, applied skills (reference to the hoisted section), and the agent's content.
  6. Topology emission. BFS from Start, building the ordered list of nodes. For each non-end node, emits its outgoing edges ordered by ascending priority, showing the destination label and condition (or "no condition — fallback").
  7. Execution rules. Eleven literal rules that explain to the LLM how to traverse the graph: how to choose a transition, what to do in parallel, when to terminate, how to count invocations per agent, what to do under ambiguity.
  8. Run persistence. Inserts a row in runs with the complete workflow_snapshot. This gives reproducible audit: the same flow + same input generates the same output.

Cycles as back-edges, not as a primitive

Cycles are allowed by construction — there is no validation that rejects them and there is no is_loop_back in the model. The compiler does not expand or unroll anything: if the topology has a cycle code-reviewer → bug-resolver → code-reviewer, that is emitted as two edges and that's it. The target LLM sees them, evaluates the conditions on each iteration, and keeps the per-agent count.

The cap comes from the agent, not from the loop: each FlowNode of type agent has a max_invocations (default 10, clamped to 50). When the LLM reaches the maximum and the topology would invoke it again, rule 7 instructs it to cut the flow and report to the user. It is soft-determinism: enforcement lives in the instructions and depends on the model honestly counting its invocations.

Stepped mode removes that dependency. With workflow.start / workflow.next the state machine lives on the server: AIOrc dispatches one step at a time, validates every requested transition against the graph's edges, counts invocations itself and cuts the run when a cap is reached. Illegal jumps are rejected with the list of legal transitions, every dispatch is recorded as ground truth (source='executed' in telemetry), and eval cases are graded against that verified path. Compiled mode remains available when a single round-trip matters more than enforcement.

Convergence: emergent, not detected

When two branches of a fork (or a conditional branching) return to a common node, the compiler does not compute convergence points or emit the common subgraph "only once". It simply lists each edge in the topology section. The LLM sees that two transitions arrive at the same node and invokes it once after consolidating the branches. This avoids costly reachable-set analysis at the cost of delegating coordination to the target model, which already has the entire graph in its context.

Structure of the compiled workflow

The compiler's output is a markdown document with five sections, ordered so the target LLM has all the information before starting to traverse the topology. We don't use "STEP" numbering or literal verdicts — the LLM is the one that decides which transition to take according to the natural-language conditions on each edge. This is the actual skeleton generated by src/orchestrator/index.ts:

# AIOrc — Multi-Agent Workflow You are the orchestrator. You have the following sub-agents available and a recommended topology to invoke them. Follow the topology as a guide; the conditions on the edges are natural-language hints — interpret them with judgment. ## User input (if any payload came in the MCP call) ```json { "request": "...", "context": "..." } ``` ## Skills (mandatory rules) // Hoisted: each skill referenced by some agent, only once. ### Skill: tdd-governance *description...* [full skill content] ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ ## Available sub-agents ### Agent: task-planner **Max invocations:** 10 **Description:** Analyzes the user's request... **Expected output format:** JSON with field 'intent' **Applied skills:** (references by name to the "Skills" section above) - *tdd-governance*: ... **Agent instructions:** [agent content] **Mandatory verification:** re-read your output and confirm it meets each applied skill before moving on to the next step. ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ ## Flow topology **Entry point (Start):** begin by invoking sub-agent *task-planner*. **Edges (allowed transitions):** - From *task-planner*: → *bug-resolver* *when:* the output indicates it is a bug → *new-feature* *when:* the output indicates it is a feature - From *bug-resolver*: → *code-reviewer* *(no condition — fallback)* - From *code-reviewer*: → *bug-resolver* *when:* verdict == FAIL and we want another iteration → 🏁 End: "merged" *when:* verdict == PASS ## Execution rules 1. Start at the entry point (Start)... 2. After each agent, evaluate the conditions on its outgoing edges and choose ONE single transition. 3. The conditions are natural language — interpret them based on the previous agent's output. 4. If an edge has no condition, it is the default fallback. 5. If ALL of them have conditions and NONE match, terminate successfully. 6. Edges back to a previous agent are legitimate — they implement loops/retries. 7. **Per-agent cap:** keep the count. If you reach the maximum and the topology wants to send you back to it, stop and report to the user. 8. **Parallel nodes (⫲):** fire ALL branches concurrently (ideally one Agent tool call per branch in the same response). Wait for all of them to finish and consolidate before moving on. 9. If you reach an End node, terminate and report its outcome. 10. If you reach an agent with no outgoing edges, terminate. 11. If you cannot decide between multiple transitions, stop and consult the user. ## Possible workflow outcomes - merged - abandoned ## Optional report When you finish, OPTIONALLY call `workflow.report` with: ```json { "runId": "...", "report": { "path": [...], "invocations_per_agent": {...}, "ended_at": "...", "final_summary": "..." } } ```

There are deliberate design decisions behind each element:

Soft determinism: why it works

AIOrc has no programmatic enforcement mechanism — there is no coordinator agent that validates each step of the target model. Flow fidelity depends entirely on the LLM respecting the compiled instructions. We call this soft determinism.

This sounds fragile, but in practice it works well for several reasons:

Empirical validation

In user tests over flows with branches (multiple edges with conditions from the same agent) and cycles (back-edges with caps via max_invocations), Claude Sonnet 4.5+ correctly interpreted the natural-language conditions and respected the caps in the cases tested, including scenarios where the request was ambiguous and required deciding intent without explicit cues.

Where soft determinism can fail (and how to mitigate it):

Comparison with 2026 frameworks

The multi-agent orchestration space changed a lot between 2024 and 2026. Claude Agent SDK (Anthropic, late 2025) and OpenAI Agents SDK (2025) appeared as official offerings from the model providers. LangGraph matured towards stateful workflows, CrewAI focused on role-based collaboration, and AutoGen (now under the AG2 community fork plus the official Microsoft Agent Framework) stays in the conversational niche.

AIOrc does not compete directly with any of them — it occupies the compile-time, single-call, MCP-native niche that no major framework currently covers:

AIOrc LangGraph Claude Agent SDK OpenAI Agents SDK CrewAI
Execution model Compile-time, 1 LLM call Runtime, N calls Runtime, N calls Runtime, N calls Runtime, N calls
Control flow Visual graph (start / agent / parallel / end) + edges with natural-language conditions + per-agent cap Conditional edges + state machine (StateGraph) Subagents + tools + programmatic hooks Handoffs between agents; per-tool guardrails Role-assigned tasks; optional manager agent
State Inline in the context (the model sees everything) Shared state (TypedDict) with reducers Persistent filesystem + context management Conversation thread + tool results Per-crew memory + task outputs
Flow definition Visual drag-and-drop, persisted as JSON Python code (nodes + edges) Code (subagent definitions + permissions) Python code with decorators Python code (Crew + Agents + Tasks)
Mid-run intervention No Yes (interrupts + checkpoints) Yes (hooks: PreToolUse, PostToolUse, etc.) Limited (callbacks) Limited
Intermediate streaming No Yes (events stream) Yes (stream API) Yes (streamed responses) Limited
Runtime required at destination MCP client only (Claude Code, Cursor, etc.) LangChain + LangGraph Claude Agent SDK + tools OpenAI Agents SDK CrewAI runtime
Target model Any MCP-compatible (optimized for Claude) Any (LangChain abstraction) Claude only OpenAI only Any (LiteLLM)
Audit workflow_snapshot and execution_report in SQLite Checkpoints + optional LangSmith Structured logs Traces via OpenAI dashboard Basic traces

"AIOrc works well where the graph is stable. If your workflow branches dynamically based on intermediate outputs you cannot predict, you need an orchestrator with a runtime (LangGraph, Claude Agent SDK). If it is well-defined and only varies in parameters, compiling it is faster and more auditable — and does not require installing anything in the target project beyond the MCP client you already have."

Key differences per framework

vs. Claude Agent SDK: Claude Agent SDK is Anthropic's official offering for building Claude-based agents (what the Claude Code team uses internally). It offers subagents, customizable tools, programmatic hooks, and context management. It is runtime-based and lives in the project's code. AIOrc, in contrast, defines the flow outside the target project — the destination only needs MCP. If you have authority over the destination's code and want fine programmatic control, Claude Agent SDK is more powerful. If you want to define flows centrally and reuse them across N projects without touching them, AIOrc is more practical.

vs. LangGraph: LangGraph assumes you will write Python to define the graph, and that you want state management with checkpoints, time travel, and human-in-the-loop. AIOrc discards all of that in exchange for a visual graph, persisted as JSON, executable in a single round-trip.

vs. OpenAI Agents SDK: Simpler than LangGraph, based on handoffs between agents. OpenAI only. AIOrc is model-agnostic in principle (any MCP client) but optimized for Claude.

vs. CrewAI: CrewAI shines when agents have to collaborate in a role-based way (researcher → writer → editor) with an optional manager. AIOrc does not think in terms of roles but in terms of steps of a graph.

Case study: bug vs feature with parallel QA

A realistic flow that exercises all four node types: start, several agent, a QA parallel, a review→fix cycle, and two end with different outcomes. All conditional logic lives in the edges, not in separate primitives:

🟢 Start │ ▼ [task-planner] (max_invocations: 5) │ ├──► when: the output indicates it is a bug │ [bug-resolver] (max_invocations: 8) │ │ │ └────┐ │ ▼ ├──► when: the output indicates it is a feature │ [new-feature] (max_invocations: 8) │ │ │ └────┐ │ ▼ │ [backend-dev] (max_invocations: 10) │ │ │ ▼ │ ⫲ Parallel: QA gates │ ├──► [code-reviewer] │ └──► [security-qa] │ │ │ ▼ │ ┌─ when: both verdicts indicate PASS │ │ 🏁 End: "merged" │ │ │ └─ when: either indicates FAIL │ ▼ │ (back-edge) → [backend-dev] // cycle, capped by max_invocations

Bug-vs-feature flow: branching by edge condition, parallel QA fork, fix cycle via back-edge, two terminal outcomes.

How it translates to real primitives

Empirical results

Tested with ambiguous prompts where the intent is not explicitly declared:

The review-fix-review cycle ran 1-3 times depending on the code-reviewer's verdict, without exceeding the cap (max_invocations: 10 on backend-dev) in any of the tests. The parallel fired both QA agents concurrently in the LLM's response, and the join consolidated the verdicts before moving on.

Technical limitations

1. Context size scales linearly with the flow

Each agent embeds its full content in the compiled output. A 12-agent flow with 2KB of content each produces a ~24KB document of instructions alone (not counting the user's request or the output the model generates). For Claude Sonnet with a 200K context, this is still marginal — but flows of 30+ agents with extensive content can approach the effective limit (where the model's performance degrades due to context rot).

Mitigation: write concise agents, lean on external skills (referenced by name instead of embedded), and split large flows into hierarchical sub-flows. Hierarchical splitting is not implemented yet in AIOrc.

2. No intermediate streaming

The end user does not see partial results until the flow ends. If an intermediate agent fails, the entire output is lost. For long tasks (more than 60 seconds of generation) this is noticeable.

Mitigation: design short flows where the cost of re-running is low, and rely on the optional workflow.report for post-hoc auditing.

3. No mid-run interruption

Once the model starts executing the compiled flow, no feedback can be injected. If the user detects an error early, they have to wait until the end.

4. Parallelism delegated to the LLM

The parallel node exists in the model and the compiler emits it with the instruction to fire via concurrent Agent tool calls, but real parallelism depends on the MCP client supporting multiple tool calls per response. If the client does not, the LLM serializes the branches — it stays correct, but loses the wall-clock gain. LangGraph and CrewAI handle parallelism in orchestrator code, not in the client.

5. The per-agent cap depends on the LLM

Each agent's max_invocations is enforced by the instructions (rule 7) and depends on the LLM honestly counting its own invocations. In practice, modern LLMs respect the cap with high fidelity because they have to write the count in their own output, but there is no programmatic enforcement: if the model "lies" about how many times it invoked something, there is no external orchestrator to cut it off.

Soft determinism

Points 1-5 are consequences of the compile-time approach. They are deliberate trade-offs, not bugs. If you need any of those capabilities as a hard requirement, an orchestrator with a runtime is the right choice.

When to use AIOrc (and when not to)

Use AIOrc when

Ideal use cases: code review pipelines, on-call playbooks, deterministic ETL, QA gates with fixed criteria, documentation generation from specs, refactor pipelines.

Do NOT use AIOrc when

For those cases: LangGraph (state-based with interrupts), CrewAI (role-based), AutoGen (conversational).

Audit logging and the EU AI Act (Article 12)

Article 12 of the EU AI Act requires high-risk AI systems to support automatic event logging across their lifetime, with logs that allow tracing how each output was produced (retention of at least six months under Articles 19/26). AIOrc's audit layer maps directly onto that requirement for agentic workflows:

This does not make a deployment "compliant" by itself — compliance is a property of the whole system — but the event log Article 12 demands is exactly what AIOrc produces as a side effect of verified execution.

Gating agent changes with evals

Evals turn "I think this change improved the agent" into a measurable verdict. The recommended working pattern:

  1. Define 3-10 eval cases per flow (Usage analytics → Evals): a fixed input, the expected End outcome, and the agents that must run.
  2. Before changing an agent or the flow, run the suite from any MCP client: call workflow.eval, then workflow.start with each case id. The server grades each run against the verified path — the model never grades its own work.
  3. Change the agent. Run the suite again.
  4. Compare pass rates in the Evals table. A drop means the change broke a path your team depends on — fix it before anyone else hits it.

Eval verdicts also appear per-run in the Audit page, so a failing case is traceable step by step.

References

[1] Anthropic — Model Context Protocol (MCP)
Specification of the protocol AIOrc implements. Defines how Claude connects to external tools via stdio or HTTP. AIOrc exposes workflow and workflow.report as MCP tools. modelcontextprotocol.io
[2] Anthropic — Claude Agent SDK
Anthropic's official offering for building programmatic Claude agents (subagents, hooks, context management). The most direct runtime "competitor" when you want fine control over each agent step. docs.anthropic.com/agent-sdk
[3] OpenAI — Agents SDK
SDK released by OpenAI in 2025 for multi-agent orchestration with handoffs and guardrails. Specific to the OpenAI ecosystem. openai.github.io/openai-agents-python
[4] LangGraph — stateful multi-agent orchestration
Orchestration loop with shared state, checkpoints, streaming, and human-in-the-loop. The most mature competitor when you need an active runtime and state persistence. langchain-ai.github.io/langgraph
[5] CrewAI — role-based multi-agent collaboration
Framework for agents with assigned roles collaborating on a shared goal. Inter-agent communication via crew memory. crewai.com
[6] Microsoft Agent Framework / AutoGen
Conversational agents that dialogue with each other. The original AutoGen line was reorganized in 2025: the community fork AG2 maintains the legacy, and Microsoft launched Agent Framework as its official offering (unifying AutoGen + Semantic Kernel). github.com/microsoft/agent-framework
[7] Pydantic AI — typed agents
Python framework that exposes agents with Pydantic validation, focused on type-safety and composition. A lightweight alternative to LangGraph for simple cases. ai.pydantic.dev
[8] Anthropic Claude Code — agentic IDE with native MCP
Reference MCP client used to test AIOrc. Integration is done via .mcp.json in the project directory. AIOrc was designed to integrate with MCP clients like this one. claude.ai/code
[9] arXiv 2604.05150 — Static Analysis of LLM-Orchestrated Programs (Apr 2026)
Recent research on static analysis of AI workflows compiled at build time. Shares the compile-time philosophy from a different angle (formal verification of flow properties). arxiv.org/abs/2604.05150
[10] Liu et al. — Lost in the Middle: How Language Models Use Long Contexts (2023)
Seminal work on context rot. Relevant to AIOrc because it embeds the entire flow in the target model's context: it empirically justifies why short flows (3-12 agents) perform better than massive ones. arxiv.org/abs/2307.03172
[11] Yao et al. — ReAct: Synergizing Reasoning and Acting in Language Models (2022)
The paper that established the Thought→Action→Observation pattern that most multi-agent frameworks implement. AIOrc avoids it: instead of runtime Thought-Action loops, it compiles the reasoning structure upfront. arxiv.org/abs/2210.03629