Loop-as-tool
Agent and voice loops can trigger other loops mid-conversation. Declare them with ctx.workflow() or ctx.voice(), build a tool list with ctx.tools({ loops }), and pass that list to ctx.OLLIE.chat (agent) or ctx.OLLIE.reply (voice).
z (Zod) is available in the loop runtime for authoring inputSchema values.
Declare and wire tools
const intakeTool = ctx.workflow({
loopId: "intake-workflow-loop-id",
description: "Start a facial scan intake for the patient",
inputSchema: z.object({
patientId: z.string(),
phoneNumber: z.string(),
}),
});
const callTool = ctx.voice({
loopId: "intake-voice-loop-id",
description: "Start a voice intake call and return a join link",
inputSchema: z.object({ reason: z.string() }),
});
const tools = ctx.tools({
loops: {
start_intake: intakeTool,
start_call: callTool,
},
});Each key under loops becomes the tool name the model calls. The description and inputSchema tell the model when and how to invoke it.
When the model calls a tool, the platform validates the arguments against inputSchema and forwards them to the target loop as event.body.
What the model gets back
The platform does not wait for the child loop to finish. The model receives trigger metadata, not the child loop’s return value.
| Tool kind | What happens | Model receives |
|---|---|---|
workflow | Starts a background workflow run | { runId, workflowInstanceId } — plus scanSessionId / scanUrl if the target loop provisions a scan |
voice | Starts a voice call (browser or outbound phone) | { runId, sessionId, joinUrl, callSid? } — share joinUrl with the caller |
This is intentional: child workflow loops may suspend (e.g. on step.waitForScan), so the parent conversation cannot block on their output.
Good fits
- Kick off long-running work — start an intake workflow, hand the patient a scan URL, continue chatting.
- Start a voice call — return a
joinUrlthe model can read out or paste into chat. - Fire-and-forget side effects — trigger a notification or audit loop where the caller does not need the result inline.
Poor fits
- Synchronous API lookups — searching a directory, fetching slots, or booking an appointment where the model needs the response data for the next turn. Workflow tools only return run identifiers; the model never sees practitioner lists, timeslots, or booking confirmations from the child run.
- Multi-step data chains — search → pick → book flows that depend on each step’s output. Put the API calls in one loop (direct
fetchinsidestep.dofor workflows, or inline logic inonTurn/onChatMessagefor voice/agent loops) instead of chaining workflow tools.
Agent vs voice
The declaration is identical. Only the OLLIE entry point differs:
| Loop type | Pass tools to… |
|---|---|
| Agent | ctx.OLLIE.chat({ tools }) |
| Voice | ctx.OLLIE.reply({ tools }) |
See Agent loops and Voice loops for the full handler shapes.
Where to go next
- Agent loops —
onChatMessage,OLLIE.chat, knowledge base. - Voice loops —
onTurn,OLLIE.reply, starting calls from workflows.