What you will build
The sample agent handles delivery questions. It greets the caller, moves to an order lookup when help is needed, records whether follow-up is needed, and ends the call. You can change the graph, save the same agent again, and place a call using a specific saved revision. You need Node.js 22.19 or later. Live mode also needs a workspace API key, wallet credit for calls, and an active phone number for the phone steps. Use an HTTPS endpoint you control for an HTTP tool. The included tool service returns one fictional order so you can test the request format.Run the example
http://localhost:3000. Demo mode works without a key and resets its
agents when the server restarts. It does not run speech, execute tools, contact
Mirai, or dial a number. Its call result is explicitly marked as sample data.
To connect your workspace, set these values in .env and restart:
VITE_, put it in localStorage, or send it to a browser.
How the editor and API fit together
Your frontend owns selection, dragging, forms, and unsaved edits. The agent’sflow field holds the graph. Its tools and input_schema fields define what
the graph can use. There is no separate flow-agent creation endpoint.
The API calls below run on your server. The repository already has the client
in server/mirai.js; this wrapper makes its returned body easier to use in the
following snippets:
Authorization: Bearer …, preserves both API error shapes,
and makes one request per operation. It does not automatically retry writes.
Step 1 Create the graph
A graph hasnodes and edges. This is a complete, small example:
data.label is its function
name; data.condition tells the model when to use it. Conditions are plain
language, not JavaScript expressions. A node change keeps the conversation
history and replaces the current instructions and offered tools.
Use 2–20 nodes and 1–40 edges, with a graph under 100 KB. Every conversation
node must be reachable from the start and have a path to an end. Node IDs and
transition labels use letters, digits, and underscores, with a letter or
underscore first. Use lowercase labels to avoid names that normalize alike.
The graph reference
lists the remaining field limits.
React Flow adds fields such as
selected, measured, and handle IDs. Strip
them when saving, as shared/graph.js does:
type: 'smoothstep' is a drawing
choice, not the conversation’s condition.
Step 2 Validate and create the agent
POST /agents/flow/validate takes the graph directly, without a { flow: … }
wrapper. It checks graph rules and model compatibility without saving or
calling. Agent creation also checks input declarations and tool references.
Flow agents use their start node’s greeting and node prompts. You do not need
system_prompt or first_message. Query GET /v2/voices for voice IDs your
deployment offers. This recipe creates a draft so phone calls wait until you
publish it.
You can also create the JSON exported from the app with cURL:
Step 3 Update the same agent without overwriting someone else’s edit
Store the returned agent ID and numericrevision. Send that revision as
if_revision on your next update:
tools
array and nested configuration blocks replace their previous values; send the
complete list or block, including entries you want to keep.
For an editor that changes only the graph, use the dedicated routes:
agent_revision, never the graph hash, for the write precondition. A
409 conflict means your copy is stale. Keep the local edits, fetch the current
agent, show the difference, and let the user reconcile it. Do not automatically
resend the old graph with a newly fetched revision. The example keeps your
edits on failure and lets you export them before reloading.
After a lost response, read the agent before another write: the first request
may have succeeded. If creation had no confirmed response, inspect the agent
list before creating again.
Step 4 Add a tool and offer it on one node
Tool definitions belong to the agent. A node carries only their names indata.tools. The delivery step can offer lookup_order while the greeting
step offers only its transitions.
First provide an HTTPS endpoint. To try the repository’s sample endpoint, put
a random TOOL_SHARED_SECRET of at least 24 characters in .env, then run
npm run tools. Expose port 3001 through an HTTPS tunnel on port 443. Keep the
editor on port 3000 private to your machine. The sample returns delivery data
only for DEMO-1042; adapt server/tool-service.js to query your database.
Save the same secret in Mirai from your backend:
shared/example.js declares an order_id argument and sends it as
{{ args.order_id }} in an HTTP POST body. Its when rule tells the model to
wait until the caller provides or confirms that number. speak_while supplies
the line spoken during the request.
Use {{ secrets.name }} in a header instead of embedding the credential in
the tool JSON. Tool URLs must satisfy the API’s outbound-request rules;
localhost and private network URLs are not reachable tool destinations.
Only start and conversation nodes can offer on-call tools, up to ten names
per node. Each name must refer to this agent’s
on_call definition. Disabled
tools are not offered. Tool names cannot collide with transition labels
anywhere in the graph. Keep end_call disabled or absent: a flow ends at an
endCall node.
You can test the saved HTTP request without making a phone call:
Step 5 Separate call inputs from extracted values
Inputs are facts your application knows before the call: a customer’s name, an order number, or a preferred language. Declare them ininput_schema and
send typed values in the call’s variables object.
{{customer_name}} in node prompts and greetings. HTTP tools use explicit
namespaces such as {{ vars.customer_name }} and {{ args.order_id }}.
Edge conditions do not interpolate variables. When an input schema is present,
declare each input your node text references.
Extraction collects facts from what the caller says. In the sample,
needs_follow_up is a boolean extracted when the delivery node is left. Its
definition lives in data.extraction_variables, with
data.extraction_enabled: true. It is not a required call input.
Extraction runs in the background, so do not depend on its result being ready
for the next node’s opening line. Use the conversation history to continue
the call and inspect the final extracted values in its results.
For incoming phone calls, your application cannot supply a fresh variables
object with each ring. Use input defaults, ask the caller for missing facts,
or fetch context with a pre-call tool. Required inputs without defaults can
prevent inbound admission. The sample defaults the name to there and the
order number to an empty string.
Step 6 Publish the saved revision
Step 7 Connect a phone number
If your deployment enables its default calling route, you can select Platform default in the app and skip number setup. Sendchannel: "phone"
and to without phone_number_id; Mirai chooses the outgoing number.
This is the route used by Console’s agent dialer. Availability depends on your
deployment: a disabled route returns trunk_unavailable; deployments requiring
verified test calls return phone_verification_required and need the dedicated
test-call endpoint or a workspace number. The app displays that API error.
To choose an outgoing number yourself or assign an inbound agent, use a
workspace number as described below.
If you already have an active workspace number, list it and skip provider
setup:
GET /v2/telephony/options before offering onboarding in your own product.
Assign the published agent for inbound calls, then activate the number if it
is not already ready and verified:
version; agents use if_revision. Read and retain each
response before the next operation. Activation configures the provider’s
voice application. If another application is already attached, stop and review
that assignment before explicitly requesting a replacement. The example
always sends replace_existing_application: false.
Inbound assignment makes incoming calls to that number use this agent. For
outbound calls, the request explicitly chooses both the agent and the number.
You can leave an existing inbound assignment in place when testing outbound
calls. The number must have state: "ready" and connection_state: "verified".
Step 8 Place the call and inspect its result
Use your own test phone and confirm that the recipient expects the call. This request rings the destination and consumes wallet credit;test: true is not
supported for phone calls.
nodes_visited and extracted variables, plus final_node
when a node was reached. Use the call’s agent_revision to identify its saved
configuration. Inspect tool receipts separately to see what actually ran.
If a call request loses its response, retry the same body with the same
idempotency key. A new key can create a second call. The app saves an unresolved
attempt in tab-scoped sessionStorage and offers Retry the same request.
Keep these records in your backend database in a hosted product.
first_message overrides are refused for flows. Personalize the start node’s
greeting through variables. A flow needs voice: text-only calls are not
supported. To add browser audio, follow the
browser calls guide. For a server
notification when final results are ready, add a webhook_url, keep
final_results: true, and verify the signed call.processed event as described
in the webhook guide.
Test it
Runnpm run check for local API-boundary tests and a production build. In
demo mode, create the agent, edit and save it, publish it, assign the demo
number, then create a demo call. Restart the server to see the empty workspace
behavior. Demo calls do not test conversation quality or provider routing.
Before using a live number, validate and save the graph, preview its inputs,
and test the order tool against the sample order. On your test call, ask for a
delivery update, provide the order ID, then end the conversation. Check the
visited nodes, extraction, and tool receipts. Make another call that needs no
help to exercise the direct path to the end node.
The example has not verified your workspace’s live credentials, provider
configuration, or audio. Complete the live checks with your test number before
giving the flow to customers.
Put the editor in your platform
The sample is a local, single-workspace app. Its loopback and same-origin guards are appropriate for that setup. A hosted product needs authenticated sessions, workspace ownership checks on every agent, call and number, CSRF protection, and rate limits. Derive the workspace and its key from the signed-in user on your server. Do not accept arbitrary API hosts or workspace keys from the page. Store unsaved drafts and call attempts in your own database. Keep the API revision with each editor session, and treat an unresolved write as unresolved until you read it back. Add request limits and role checks around phone activation, assignment, publication, and calls. Call inputs, transcripts, tool receipts, and exported JSON can contain customer information. Apply your product’s access and retention policies. This sample disables call recording, but that does not turn off transcripts or stored call results. Follow the calling, consent, and recording requirements that apply to your use case; see the limits guide. Start by adaptingshared/example.js and the forms in src/main.jsx. Keep
the server boundary and revision handling when you move the editor into your
application. The full source
includes the sample tool, error handling, and tests.