Skip to main content
You can put a flow editor in your own product and save its graph directly on a Mirai voice agent. Your users arrange conversation steps, write the conditions between them, attach tools, and connect a phone number. Mirai runs the saved conversation when a call starts. The example app on GitHub uses React Flow for the canvas and Express for the backend. It includes a working local demo and a live mode that sends requests to your workspace.

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

Open 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:
Use the API host your key was issued for. The key stays on the server. Do not prefix it with 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’s flow 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:
The client sends 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 has nodes and edges. This is a complete, small example:
Each edge becomes a function the model can call. 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:
Keep positions so reopening an agent restores its layout. Keep canvas state separate from your API payload; React Flow’s 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 numeric revision. Send that revision as if_revision on your next update:
When you edit tools, inputs, and the graph together, send them in one PATCH. This lets the API validate the resulting configuration together. The 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:
Use 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 in data.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:
Replace the sample hostname before saving. The definition in 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:
This executes your real endpoint. A tool that writes to a CRM would perform that write during a test too. The app’s demo mode does not execute this request. See the tools reference for conditions, response transforms, asynchronous execution, and receipts.

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 in input_schema and send typed values in the call’s variables object.
Use {{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

An agent has one current configuration. Publishing saves a new non-draft revision. Saving edits to a published agent changes what new ordinary direct calls use. Setting it back to draft pauses ordinary direct call admission; there is no separate published copy that stays active behind it. Already accepted calls keep their snapshots. For a substantial redesign of a busy agent, create a separate agent, test it, and switch your platform’s agent mapping or number assignment when ready. The agent guide covers revision history and rollback, which creates a new draft 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. Send channel: "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:
To bring a number from your Plivo account, the provider import sequence is:
Creation verifies the provider credentials. Number import requires a verified integration and ownership of that number. These credentials stay on your backend. The local app starts with imported workspace numbers; it does not collect Plivo credentials or purchase numbers. Provider features and number availability depend on your deployment and workspace setup. Inspect 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:
Phone numbers use 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.
Creating a call returns an ID, not a completed conversation. Refresh the call until it reaches a terminal status; results may arrive after it ends. A flow result includes 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

Run npm 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 adapting shared/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.