Composing agents and workflows
In Culvii, agents and workflows are peers. Both are deployable units. Both can be invoked. Both can call the other.
This is a deliberate design choice, and it's the one most likely to confuse developers coming from frameworks where "agents" and "workflows" are different categories of thing. This page is the conceptual model.
The core idea
- A workflow is a deterministic graph: this step, then that step, branch here, gate there. The author decides the structure. Each step might invoke an agent.
- An agent is an autonomous reasoner: it has tools and a goal, and the LLM decides which tools to call in what order. The structure emerges from the model.
Both are top-level deployable artifacts. You can deploy a workflow alone, an agent alone, or both. You can run any of them via culvii run or trigger them from outside Culvii.
The composition rules:
- A workflow can have an agent as one of its steps.
- A workflow can have another workflow as one of its steps (sub-workflow).
- An agent can have a tool that wraps a workflow. From the agent's perspective the workflow is just another tool.
- An agent can have a tool that wraps another agent. Same idea.
Combined, you get hierarchies that are easy to extend without rewriting.
When to reach for a workflow
Use a workflow when:
- The structure is known and deterministic.
- You need explicit branches, parallel steps, or loops.
- You need a human-in-the-loop gate.
- You want operator visibility into the structure of the run, not just its outcome.
- You want testability per step.
A workflow is the right answer for "first do A, then if X do B otherwise do C, then ask a human, then do D." That logic is yours, not the LLM's.
When to reach for an agent
Use an agent when:
- You can't enumerate the steps in advance.
- The right tool to call depends on what previous tool calls returned.
- You want the LLM to reason its way to a result you can validate with a typed schema.
An agent is the right answer for "given this messy input, figure out the right vendor and explain why." The LLM picks the path.
Patterns
Pattern 1 — workflow with an agent step
The most common shape. Use a workflow as the spine for deterministic logic; embed agents at the points where you need reasoning.
const screen = workflow({
name: 'vendor-screen',
steps: [
step('fetch', { tool: lookupVendor, output: 'vendor' }),
step('analyze', { agent: vendorAnalyst, output: 'screening' }),
branch('proceed?', {
condition: (s) => s.screening.decision === 'pass',
true: 'notify',
false: 'reject',
}),
step('notify', { tool: notifyApproval }),
step('reject', { tool: notifyRejection }),
],
})
The workflow holds the structure. The agent does one piece of reasoning inside it.
Pattern 2 — agent with a workflow as a tool
Sometimes the agent needs to delegate a complex sub-task that itself has structure. Wrap the workflow as a tool from the agent's perspective.
const runVendorScreen = tool({
name: 'run-vendor-screen',
description: 'Run the vendor screening workflow for a given vendor ID.',
input: z.object({ vendorId: z.string() }),
output: VendorScreenResultSchema,
execute: async (input) => {
return await runWorkflow(vendorScreen, input)
},
})
const procurementCoordinator = agent({
name: 'procurement-coordinator',
goal: 'Coordinate procurement for a project, including vendor screening and contract drafting.',
tools: [runVendorScreen, draftContract, fetchProjectBrief],
// …
})
The agent can now invoke the screening workflow as part of its reasoning. From the agent's perspective, calling a workflow looks like calling any other tool.
Pattern 3 — agent calling another agent
Same shape as Pattern 2 but the wrapped thing is an agent instead of a workflow.
const askLegalReviewer = tool({
name: 'ask-legal-reviewer',
description: 'Ask the legal reviewer agent to assess contract terms.',
input: z.object({ contractDraft: z.string() }),
output: LegalReviewSchema,
execute: async (input) => {
return await runAgent(legalReviewer, input)
},
})
Useful when you want one agent to consult a specialist agent.
Pattern 4 — workflow inside a workflow (sub-workflow)
When two workflows share a common sub-structure, factor it out:
step('screen', { workflow: vendorScreen, output: 'screening' }),
Sub-workflows run in the same execution context. The audit trail and traces show the nested structure clearly.
Anti-patterns
A few shapes we'd push back on:
- One giant agent with twenty tools and no workflow. When you can articulate the structure, articulate it. Workflows give operators visibility and give you testability.
- A workflow that wraps a single agent and does nothing else. Just deploy the agent.
- Deeply nested agent-of-agents trees. Beyond two levels, you usually want a workflow at the top instead.
- Using
gate()inside an agent's tool. Gates belong in workflows. The agent should not be the unit that pauses for human approval.
What gets a DID, what gets traced
Every agent gets a W3C DID at creation. Every workflow run gets a unique run ID. When agents call workflows or other agents, the call chain is captured in traces — you can see the full hierarchy in the Console.
Tools don't get DIDs; they're not autonomous. The tool call is attributed to the agent that made it.
Performance notes
Composition is cheap. There's no measurable overhead to wrapping an agent as a tool or a workflow as a tool versus calling them directly. The engine handles the indirection in-process.
The thing that costs latency is model calls — every time an agent reasons, that's one round-trip to the model provider. Workflows don't add round-trips; only agents do.