The Ferni Workflow Engine lets you build multi-step automations that voice agents can execute. From simple sequences to complex DAGs with parallel branches, workflows bring your integrations to life.
What is a Workflow?
A workflow is a directed graph of nodes connected by edges. Each node performs an action:
- MCP Call - Execute a tool from an MCP server
- Webhook - Call an external HTTP endpoint
- LLM Prompt - Generate text with the language model
- Condition - Branch based on an expression
- Parallel - Execute multiple branches simultaneously
- Wait - Pause for time or an event
Basic Workflow Structure
{
"name": "Customer Lookup",
"trigger": {
"type": "voice_command",
"config": { "command": "look up customer" }
},
"nodes": [
{ "id": "start", "type": "start" },
{ "id": "lookup", "type": "mcp_call", "config": {...} },
{ "id": "respond", "type": "llm_prompt", "config": {...} },
{ "id": "end", "type": "end" }
],
"edges": [
{ "sourceId": "start", "targetId": "lookup" },
{ "sourceId": "lookup", "targetId": "respond" },
{ "sourceId": "respond", "targetId": "end" }
],
"entryNodeId": "start"
}
Creating Your First Workflow
Let's build a "Daily Standup" workflow that:
- Fetches today's calendar events
- Checks if there are meetings
- Summarizes them for the user
Step 1: Define the Trigger
{
"trigger": {
"type": "voice_command",
"config": {
"command": "start standup"
}
}
}
Trigger types include:
voice_command- Triggered by speechschedule- Cron-based schedulingevent- Triggered by webhook eventsapi- Triggered via API call
Step 2: Add Nodes
{
"nodes": [
{
"id": "start",
"type": "start",
"name": "Start"
},
{
"id": "fetch-calendar",
"type": "mcp_call",
"name": "Fetch Calendar",
"config": {
"serverId": "mcp_google_calendar",
"toolName": "calendar.getEvents",
"arguments": {
"date": "{{today}}",
"maxResults": 10
}
}
},
{
"id": "check-meetings",
"type": "condition",
"name": "Has Meetings?",
"config": {
"expression": "fetch-calendar.result.events.length > 0"
}
},
{
"id": "summarize",
"type": "llm_prompt",
"name": "Summarize Meetings",
"config": {
"prompt": "Summarize these meetings for a quick standup briefing:\n\n{{fetch-calendar.result.events | json}}",
"outputVariable": "summary"
}
},
{
"id": "no-meetings",
"type": "set_variable",
"name": "No Meetings Message",
"config": {
"variable": "summary",
"value": "No meetings scheduled for today. Your calendar is clear!"
}
},
{
"id": "speak",
"type": "speak",
"name": "Tell User",
"config": {
"text": "{{summary}}"
}
},
{
"id": "end",
"type": "end",
"name": "End"
}
]
}
Step 3: Connect with Edges
{
"edges": [
{ "id": "e1", "sourceId": "start", "targetId": "fetch-calendar" },
{ "id": "e2", "sourceId": "fetch-calendar", "targetId": "check-meetings" },
{ "id": "e3", "sourceId": "check-meetings", "targetId": "summarize", "condition": "true" },
{ "id": "e4", "sourceId": "check-meetings", "targetId": "no-meetings", "condition": "false" },
{ "id": "e5", "sourceId": "summarize", "targetId": "speak" },
{ "id": "e6", "sourceId": "no-meetings", "targetId": "speak" },
{ "id": "e7", "sourceId": "speak", "targetId": "end" }
]
}
Step 4: Create via API
curl -X POST https://api.ferni.ai/api/v2/developers/workflows \
-H "Authorization: Bearer pk_live_xxx" \
-H "Content-Type: application/json" \
-d '{
"name": "Daily Standup",
"description": "Summarizes today calendar for morning standup",
"trigger": {
"type": "voice_command",
"config": { "command": "start standup" }
},
"nodes": [...],
"edges": [...],
"entryNodeId": "start",
"enabled": true
}'
Node Types Reference
MCP Call
Execute a tool from a registered MCP server:
{
"id": "lookup",
"type": "mcp_call",
"config": {
"serverId": "mcp_crm",
"toolName": "lookup_customer",
"arguments": {
"query": "{{input.customerName}}"
}
}
}
Results are available as {nodeId}.result.
Webhook
Call an external HTTP endpoint:
{
"id": "notify-slack",
"type": "webhook",
"config": {
"url": "https://hooks.slack.com/services/xxx",
"method": "POST",
"headers": {
"Content-Type": "application/json"
},
"body": {
"text": "Workflow completed: {{workflowName}}"
}
}
}
LLM Prompt
Generate text using the language model:
{
"id": "summarize",
"type": "llm_prompt",
"config": {
"prompt": "Summarize this customer interaction:\n\n{{transcript}}",
"model": "gemini-2.0-flash",
"outputVariable": "summary"
}
}
Condition
Branch based on an expression:
{
"id": "check-priority",
"type": "condition",
"config": {
"expression": "ticket.priority === 'high'"
}
}
Connect with conditional edges:
{ "sourceId": "check-priority", "targetId": "urgent-flow", "condition": "true" },
{ "sourceId": "check-priority", "targetId": "normal-flow", "condition": "false" }
Parallel
Execute multiple branches simultaneously:
{
"id": "parallel-fetch",
"type": "parallel",
"config": {
"branches": [
{ "entryNodeId": "fetch-calendar" },
{ "entryNodeId": "fetch-tasks" },
{ "entryNodeId": "fetch-emails" }
]
}
}
All branches must complete before continuing.
Wait
Pause execution:
{
"id": "wait-approval",
"type": "wait",
"config": {
"duration": 60000, // Wait 60 seconds
"event": "approval.received" // Or wait for event
}
}
Set Variable
Store a value for later use:
{
"id": "set-status",
"type": "set_variable",
"config": {
"variable": "ticketStatus",
"value": "resolved"
}
}
Activity
Log a custom activity:
{
"id": "log-completion",
"type": "activity",
"config": {
"type": "workflow_completed",
"name": "Daily Standup",
"data": {
"meetingCount": "{{fetch-calendar.result.events.length}}"
}
}
}
Variable Interpolation
Use {{expression}} syntax to reference:
- Input variables:
{{input.customerName}} - Node results:
{{nodeId.result}} - Workflow variables:
{{variableName}} - Built-ins:
{{today}},{{now}},{{userId}}
Filters
Apply transformations with pipe syntax:
{{data | json}} // JSON stringify
{{text | uppercase}} // Uppercase
{{list | first}} // First item
{{number | round}} // Round number
{{date | format('MMM d')}} // Format date
Error Handling
Per-Node Error Handling
{
"id": "risky-operation",
"type": "webhook",
"config": {...},
"onError": {
"type": "goto",
"targetNodeId": "error-handler"
}
}
Retry Policy
{
"id": "flaky-api",
"type": "webhook",
"config": {...},
"retry": {
"maxAttempts": 3,
"backoffMs": 1000,
"backoffMultiplier": 2
}
}
Global Error Handler
{
"errorHandler": {
"nodeId": "global-error-handler"
}
}
Testing Workflows
Test via API
curl -X POST https://api.ferni.ai/api/v2/developers/workflows/wf_xxx/test \
-H "Authorization: Bearer pk_live_xxx" \
-H "Content-Type: application/json" \
-d '{
"input": {
"customerName": "Acme Corp"
}
}'
Dry Run
Execute without side effects:
curl -X POST https://api.ferni.ai/api/v2/developers/workflows/wf_xxx/test \
-H "Authorization: Bearer pk_live_xxx" \
-d '{
"dryRun": true,
"input": {...}
}'
Execution History
List Executions
curl https://api.ferni.ai/api/v2/developers/workflows/wf_xxx/executions \
-H "Authorization: Bearer pk_live_xxx"
Execution Details
curl https://api.ferni.ai/api/v2/developers/workflows/wf_xxx/executions/exec_yyy \
-H "Authorization: Bearer pk_live_xxx"
{
"success": true,
"data": {
"id": "exec_yyy",
"workflowId": "wf_xxx",
"status": "completed",
"triggeredBy": "voice",
"startedAt": "2026-01-11T10:00:00Z",
"completedAt": "2026-01-11T10:00:05Z",
"duration": 5000,
"nodeResults": {
"fetch-calendar": {
"status": "completed",
"duration": 1200,
"result": {...}
},
"summarize": {
"status": "completed",
"duration": 2100,
"result": {...}
}
}
}
}
Real-World Examples
Customer Onboarding Flow
{
"name": "Customer Onboarding",
"trigger": { "type": "voice_command", "config": { "command": "onboard new customer" } },
"nodes": [
{ "id": "start", "type": "start" },
{ "id": "get-info", "type": "llm_prompt", "config": {
"prompt": "Extract customer name, email, and company from: {{input.transcript}}"
}},
{ "id": "create-crm", "type": "mcp_call", "config": {
"serverId": "mcp_crm",
"toolName": "create_customer",
"arguments": { "data": "{{get-info.result}}" }
}},
{ "id": "send-welcome", "type": "webhook", "config": {
"url": "https://api.sendgrid.com/v3/mail/send",
"method": "POST",
"body": { "to": "{{get-info.result.email}}", "template": "welcome" }
}},
{ "id": "confirm", "type": "speak", "config": {
"text": "I've added {{get-info.result.name}} to your CRM and sent them a welcome email."
}},
{ "id": "end", "type": "end" }
],
"edges": [
{ "sourceId": "start", "targetId": "get-info" },
{ "sourceId": "get-info", "targetId": "create-crm" },
{ "sourceId": "create-crm", "targetId": "send-welcome" },
{ "sourceId": "send-welcome", "targetId": "confirm" },
{ "sourceId": "confirm", "targetId": "end" }
]
}
Scheduled Report
{
"name": "Daily Sales Report",
"trigger": {
"type": "schedule",
"config": { "cron": "0 9 * * 1-5" }
},
"nodes": [
{ "id": "start", "type": "start" },
{ "id": "fetch-sales", "type": "mcp_call", "config": {
"serverId": "mcp_analytics",
"toolName": "get_sales_summary",
"arguments": { "period": "yesterday" }
}},
{ "id": "generate-report", "type": "llm_prompt", "config": {
"prompt": "Generate a brief sales report from: {{fetch-sales.result}}"
}},
{ "id": "send-slack", "type": "webhook", "config": {
"url": "{{secrets.SLACK_WEBHOOK}}",
"body": { "text": "📊 *Daily Sales Report*\n{{generate-report.result}}" }
}},
{ "id": "end", "type": "end" }
]
}
Best Practices
- Keep workflows focused - One workflow, one purpose
- Use meaningful node names - Makes debugging easier
- Handle errors gracefully - Add error handlers for critical paths
- Test with dry runs - Validate before production
- Monitor executions - Check logs for failures
- Version your workflows - Track changes over time
Next Steps
- MCP Integration Guide - Connect external tools
- Webhook Events - Trigger workflows from events
- API Reference - Full endpoint documentation
Need help building workflows? Join our Discord community!