Composing agents and workflows
The core idea
Workflows and agents call into each other, but not symmetrically. The shapes are different, and knowing which one you're building matters.
A workflow is a deterministic graph you author: this step, then that step, branch here. Reach for one when the structure is known ahead of time: explicit branches, a human approval step, a fixed sequence you want operators to see the shape of, not just the outcome. "First fetch the vendor, then screen it, then if it fails route to rejection" is workflow logic; it's yours, not the model's.
An agent (a MultiAgentEngine primary, optionally with secondaries) is the opposite: the model decides which tools to call and in what order, constrained by what you gave it. Reach for one when you can't enumerate the steps up front. The right tool to call depends on what the previous one returned, or you want the LLM to reason its way to something you validate afterward rather than something you dictated in advance.
The two meet through a core.agent step: it doesn't inline agent logic, it just references a deployed agent by key, sends it a prompt, and waits for a structured response.
The three real composition patterns
1. A workflow step invokes a deployed agent
const screenVendor = new Step({
name: 'Screen Vendor',
type: 'core.agent',
params: { agentReferenceKey: 'vendor-analyst', prompt: '={{$json}}' },
})
fetchVendor.connectTo(screenVendor)
The step pauses the workflow branch on the agent channel and resumes once the agent produces a response matching the expected output schema.
2. An agent calls a workflow as a tool
Wrap an existing workflow with defineWorkflowTool() and attach it to an agent's tools:
const runVendorScreen = defineWorkflowTool({
name: 'runVendorScreen',
description: 'Run the vendor screening workflow for a given vendor ID.',
workflowId: 'vendor-screen',
triggerNodeName: 'Manual Trigger',
})
From the agent's side this looks like any other tool call. The model decides when to reach for it.
:::warning A workflow-tool call has 5 minutes to resolve If the workflow it triggers pauses on a wait surface for a person, and the approval doesn't land within that window, the agent's tool call fails with an error rather than waiting. Don't block the call on an approval that might take longer than that. Resolve it immediately and notify the agent (or the user) back through a separate channel once the approval actually lands.
Why the limit exists: a tool call is synchronous from the agent's point of view, and the agent's own run is capped at 10 minutes, shorter than that, actually, so a hung tool call can't eat the entire run on its own. Letting a single call wait indefinitely would fail silently and unpredictably instead of failing fast with a clear error. :::
3. A primary agent delegates to a secondary agent
This is Culvii's actual "agent calls agent" shape. It isn't a generic tool wrapper, it's a first-class field on MultiAgentEngine:
new MultiAgentEngine({
id: 'procurement-coordinator',
role: 'primary',
systemPrompt: 'Coordinate procurement. Delegate vendor risk checks to Risk Analyst.',
model: { provider: 'anthropic', modelId: 'claude-sonnet-4-5', credentialId: 'anthropic-credential' },
secondaryAgents: [
{
id: 'risk-analyst',
role: 'secondary',
systemPrompt: 'Assess vendor risk given the details the coordinator provides.',
model: { provider: 'anthropic', modelId: 'claude-haiku-4-5', credentialId: 'anthropic-credential' },
},
],
})
The secondary's id is part of the delegation contract. Write its prompt as a narrow job description, not a general-purpose assistant.
How deep you can nest
A reference chain (workflow calls agent calls workflow calls agent, and so on) is capped at three nodes. Only two shapes are valid:
workflow → agent → workflow: the agent's workflow tool has to point at a leaf workflow, one with nocore.agentnode inside it.agent → workflow → agent: the workflow'score.agentnode has to point at a leaf agent, and that leaf (including its secondaries) can't own any workflow tools.
Either pattern can start from a primary agent or its secondary. What matters is the chain, not which agent in it owns the tool. Anything deeper than three, a fourth level in either direction, fails validation. This isn't a style preference: culvii dev reports it live as a sync error, and culvii deploy rejects it the same way, both pointing at "fix the reference chain" before anything ships.
Anti-patterns
- One agent with every permission granted, no workflow around it. If you can articulate the structure, put it in a workflow. It's what gives operators visibility and gives you testability per step.
- A workflow that's just a wrapper around one agent step and nothing else. Deploy the agent directly.
- A workflow tool call that depends on a slow human approval. The 5-minute tool-call timeout will fail it. Resolve the tool call fast and notify asynchronously instead.
What actually gets audited
Every deploy, agent invocation, and workflow resume lands in the audit log, attributed to the user who triggered it, not to a separate per-agent identity yet. See Identity and audit attribution for the honest version of that story.