Skip to Content
ConceptsLoop-as-tool

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 kindWhat happensModel receives
workflowStarts a background workflow run{ runId, workflowInstanceId } — plus scanSessionId / scanUrl if the target loop provisions a scan
voiceStarts 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 joinUrl the 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 fetch inside step.do for workflows, or inline logic in onTurn / onChatMessage for voice/agent loops) instead of chaining workflow tools.

Agent vs voice

The declaration is identical. Only the OLLIE entry point differs:

Loop typePass tools to…
Agentctx.OLLIE.chat({ tools })
Voicectx.OLLIE.reply({ tools })

See Agent loops and Voice loops for the full handler shapes.

Where to go next

  • Agent loopsonChatMessage, OLLIE.chat, knowledge base.
  • Voice loopsonTurn, OLLIE.reply, starting calls from workflows.
Last updated on