Building workflows with JSON
Every workflow in MultiverseAI is a JSON document. You can write one by hand, generate it from a script, keep it in version control, and paste it straight into the builder — or export what you built on the canvas and edit it as text. This page is the complete reference for that format.
Overview
JSON editor
The { } button in the builder toolbar opens the current workflow as editable JSON. Edit it, validate live, then Apply to canvas.
Export
Downloads the workflow on the canvas as a .json file — including unsaved changes, and before the workflow has ever been saved.
Import
Reads a .json file back in. The document is validated before anything is written, so a malformed file is rejected with a specific reason.
A workflow is a directed graph: nodes are the steps, edges are the connections between them. Execution starts at a trigger node and follows the edges. That is the whole model — everything below is detail about how to spell it.
Quick start
{
"nodes": [
{ "id": "start", "type": "trigger" },
{ "id": "reply", "type": "aiAgent" }
],
"edges": [
{ "source": "start", "target": "reply" }
]
}Notice what is missing: there is no position, no data, no edge id, and no version wrapper. Every one of those is optional. The builder places the nodes down the canvas, fills each node's data with that type's defaults, and generates the edge id — telling you what it did in the warnings panel.
Write only the fields you actually want to control. Add detail as you need it.
Document shape
1. The export envelope
This is what Export produces and what the editor shows you.
{
"version": "1.0",
"exportedAt": "2026-08-06T09:00:00.000Z",
"workflow": {
"name": "My Workflow",
"description": "What this workflow does",
"nodes": [ /* … */ ],
"edges": [ /* … */ ]
}
}| Field | Type | Required | Notes |
|---|---|---|---|
version | string | No | Format version. A different value still imports, with a warning telling you to re-export. |
exportedAt | string | No | ISO timestamp, written on export. Ignored on import. |
workflow.name | string | No | Defaults to "Untitled Workflow". Applying JSON renames the open workflow. |
workflow.description | string | No | Free text. Defaults to empty. |
workflow.nodes | Node[] | Yes | The steps. May be empty, but the key must be present. |
workflow.edges | Edge[] | No | The connections. Defaults to an empty list. |
2. A bare workflow
Drop the envelope and put name, nodes and edges at the top level.
{
"name": "My Workflow",
"nodes": [ /* … */ ],
"edges": [ /* … */ ]
}3. A bare graph
Just nodes (and optionally edges). This is the form to generate from a script.
{
"nodes": [ /* … */ ]
}The node object
{
"id": "summarise",
"type": "aiAgent",
"position": { "x": 250, "y": 350 },
"data": {
"label": "Summarise the posts",
"model": "claude-3-5-haiku-20241022",
"systemPrompt": "Summarise the input as five bullet points.",
"temperature": 0.3,
"maxTokens": 1024
}
}| Field | Type | Required | Notes |
|---|---|---|---|
id | string | Yes | Unique within the document. Edges reference it, so pick something readable — "summarise" beats "node_3". |
type | string | Yes | One of the values in the catalog. An unrecognised type is an error, with a suggestion when it looks like a typo. |
position | { x: number, y: number } | No | Canvas coordinates in pixels; x grows right, y grows down. Omit it and nodes are stacked 150px apart at x = 250. |
data | object | No | Type-specific configuration, merged over the type defaults. Anything you leave out keeps its default. |
data.label | string | No | The name shown on the canvas. Defaults to the node type’s display name. |
style / hidden / parentId / extent | various | No | Optional canvas presentation fields. Preserved on round-trip, safe to ignore. |
Defaults are merged, not replaced
Supplying data does not wipe the rest of the configuration — your keys are layered on top of the type's defaults. Writing this:
{
"nodes": [
{ "id": "ai", "type": "aiAgent" }
]
}produces a fully configured node:
{
"id": "ai",
"type": "aiAgent",
"position": { "x": 250, "y": 50 },
"data": {
"label": "AI Agent",
"nodeLabel": "Process with AI",
"model": "claude-3-5-haiku-20241022",
"systemPrompt": "",
"temperature": 0.7,
"maxTokens": 2048
}
}So to change one setting, write one key. The catalog lists every default.
The edge object
{
"id": "e2",
"source": "scrape",
"target": "summarise",
"sourceHandle": null,
"targetHandle": null,
"type": "default"
}| Field | Type | Required | Notes |
|---|---|---|---|
source | string | Yes | The id of the node the data comes from. Must exist in this document. |
target | string | Yes | The id of the node the data goes to. Must exist in this document. |
id | string | No | Unique within the document. Generated for you if omitted. |
sourceHandle | string | null | No | Which output of the source node to leave from. Only meaningful for branching nodes. |
targetHandle | string | null | No | Which input of the target node to arrive at. Almost always omitted. |
type | string | No | Edge rendering style. Defaults to "default". |
An edge pointing at an id that does not exist is an error, not a silent drop — a typo in an id would otherwise quietly disconnect part of your workflow.
Branching
| Node type | Valid sourceHandle | Notes |
|---|---|---|
condition | "true", "false" | Two outputs — the comparison result. |
filter | "pass", "fail" | Items that matched, and those that did not. |
switch | "output-0", "output-1", … | One per output; `data.outputs` sets how many (default 3). |
parallel | "branch-0", "branch-1", … | One per entry in `data.branches` (default 2). |
{
"edges": [
{ "id": "yes", "source": "check", "target": "approve", "sourceHandle": "true" },
{ "id": "no", "source": "check", "target": "reject", "sourceHandle": "false" }
]
}If you leave sourceHandle off an edge that comes from a branching node, the builder assigns handles in the order the edges appear — first edge "true", second "false", and so on — and warns you that it did. That is convenient while drafting and fragile afterwards: reordering the edges silently swaps the branches. Set the handle explicitly in anything you intend to keep.
Expressions
{
"id": "email",
"type": "sendEmail",
"data": {
"to": "{{$json.customer.email}}",
"subject": "Your order {{$json.orderId}}",
"body": "Processed at {{$now}} by workflow {{$workflow.name}}."
}
}| Field | Type | Required | Notes |
|---|---|---|---|
{{$json.field}} | any | No | A field from the current item coming into this node. The one you will use most. |
{{$items}} / {{$item(i)}} | array / object | No | All incoming items, or the item at index i. |
{{$itemIndex}} | number | No | Index of the item currently being processed. |
{{$node["id"].json.field}} | any | No | Output of any earlier node by id — not just the one directly upstream. |
{{$workflow.id}} / {{$workflow.name}} | string | No | The running workflow. |
{{$execution.id}} / {{$execution.mode}} | string | No | This run. Mode is manual, trigger, webhook or integrated. |
{{$now}} / {{$today}} | Date | No | Current timestamp, and midnight this morning. |
Expressions are evaluated in a sandbox. Anything that reaches outside it — fetch, require, process, window, constructor and similar — is rejected, and expressions are capped at 2000 characters. For real logic, use a codeExecution node instead of a long expression.
Validation rules
Errors — these must be fixed
- The document is not valid JSON, or the top level is not an object.
nodesis missing, ornodes/edgesis not an array.- A node has no
id, or two nodes share one. - A node has no
type, or a type the canvas cannot render. - A
positionis present but itsx/yare not numbers. datais present but is not an object.- An edge is missing
source/target, points at an id that does not exist, or reuses an edge id. - The document has more than 500 nodes.
Structural rules
These match what the canvas refuses to let you draw by hand, so importing JSON cannot produce a graph you could not have built with the mouse.
- No loops. The connections must flow one way. A cycle is reported with the path that closes it (
a → b → c → a) — a cyclic workflow would never finish. To repeat work, use a loop node (loopOverItems,iterator) rather than an edge pointing backwards. - No self-connections. A node cannot feed its own input.
- No trigger-to-trigger edges. Triggers start a workflow, so they cannot feed each other.
- One connection per source/target pair. An exact duplicate is dropped with a warning. Two edges sharing a source and target but using different
sourceHandlevalues are fine — that is how both branches of a condition converge on the same node.
Warnings — applied automatically
- A missing
position— the node is placed down the canvas. - A missing
data— filled from the type defaults. - A missing edge
id— generated from the endpoints. - A missing
sourceHandleon a branching node — inferred by edge order. - A duplicate connection — the redundant edge is dropped.
- A non-string
data.label— the type default is used. - A
versionthat is not 1.0. - No trigger node — the workflow imports, but nothing will start it.
Applying JSON replaces the entire canvas, and nothing is written to your account until you press Save. Export first if you want a copy of what is there now.
Node type catalog
type accepts, with the data fields each one fills in by default. Omit a field and you get the default shown here. This list is generated from the running app, so it always matches what the builder will accept.Actions
"action"ActionPerform various actions
data fields (default)
AI & Logic
"aiAgent"AI AgentAI-powered processing
data fields (default)
"condition"ConditionIf/then logic
data fields (default)
"filter"FilterFilter data based on conditions
data fields (default)
"switch"SwitchMulti-branch routing logic
data fields (default)
AI/ML Enhancement
"aiEmbeddings"AI EmbeddingsConvert text into high-dimensional vector embeddings
data fields (default)
"aiTransform"AI TransformAI-powered data transformation and processing
data fields (default)
"contentSummarization"Content SummarizationGenerate intelligent summaries from long-form content
data fields (default)
"multiAIProvider"Multi-AI ProviderOrchestrate multiple AI providers for improved reliability
data fields (default)
"semanticSearch"Semantic SearchIntelligent semantic search with vector similarity
data fields (default)
"vectorDatabase"Vector DatabaseStore, search, and manage high-dimensional embeddings
data fields (default)
Canvas
"stickyNote"stickyNoteAdded from the canvas, not the node palette.
data fields (default)
Communication
"angelsend"AngelSendRun any AngelSend email-outreach action: campaigns, steps, pause/resume/stop one recipient, merge vars, sender limits, opt-outs, webhooks
data fields (default)
"chatInterface"Chat InterfaceSend to Slack, Discord, Teams, Google Chat, Mattermost or Telegram
data fields (default)
"ftp"FTP/SFTPTransfer files via FTP, SFTP, or FTPS
data fields (default)
"graphql"GraphQLExecute GraphQL queries, mutations, and subscriptions
data fields (default)
"meetingNotetaker"Meeting NotetakerSend an AI notetaker into a Google Meet, Zoom, Teams or Webex call, or read a meeting's summary, action items and transcript
data fields (default)
"rssFeed"RSS FeedFetch and process RSS/Atom feeds
data fields (default)
"sendEmail"Send EmailSend emails via SMTP, Gmail, or other providers
data fields (default)
"slack"SlackPost messages to Slack via your connected workspace or an incoming webhook
data fields (default)
"ssh"SSH CommandExecute commands on remote servers via SSH
data fields (default)
"webhookResponse"Webhook ResponseSend custom HTTP responses back to webhook triggers
data fields (default)
Data Processing
"codeExecution"Code ExecutionExecute custom JavaScript code
data fields (default)
"compareDatasets"Compare DatasetsCompare two datasets and find differences
data fields (default)
"convertToFile"Convert to FileConvert data to various file formats
data fields (default)
"crypto"CryptoEncryption, hashing, and cryptographic operations
data fields (default)
"databaseQuery"Database QueryExecute SQL queries with a visual editor and result preview
data fields (default)
"dateTime"Date & TimeDate and time operations
data fields (default)
"extractFromFile"Extract From FileExtract data from PDF, Excel, CSV files
data fields (default)
"jsonOperations"JSON OperationsParse, transform, and manipulate JSON data
data fields (default)
"removeDuplicates"Remove DuplicatesRemove duplicate items from datasets
data fields (default)
Data Sources
"gmail"GmailRead, search, send, and manage emails via Gmail API
data fields (default)
"googleCalendar"Google CalendarCreate, list and update events, add Meet links, check availability
data fields (default)
"googleChat"Google ChatPost and manage messages in Google Chat spaces as you
data fields (default)
"googleContacts"Google ContactsLook up, add and update Google Contacts
data fields (default)
"googleDocs"Google DocsCreate, read, and export Google Docs documents
data fields (default)
"googleDrive"Google DriveList, upload, download, and manage files in Google Drive
data fields (default)
"googleForms"Google FormsBuild forms and read their responses
data fields (default)
"googleMeet"Google MeetCreate Google Meet links
data fields (default)
"googlePhotos"Google PhotosBrowse albums and media items from Google Photos
data fields (default)
"googleSearch"Google SearchRaw Google results (web, news or images) via Serper
data fields (default)
"googleSheets"Google SheetsRead, write, append and look up rows in Google Sheets
data fields (default)
"googleSlides"Google SlidesCreate decks, add slides and fill {{placeholders}} in Google Slides
data fields (default)
"googleTasks"Google TasksCreate, list and complete Google Tasks
data fields (default)
"promptInput"Prompt InputUse saved prompt and response as input
data fields (default)
"tiktok"TikTokManage TikTok account, videos, and engagement
data fields (default)
"webscraper"Web ScraperExtract content from websites using Firecrawl
data fields (default)
"websearch"Web SearchAI-powered web search with Perplexity
data fields (default)
"youtube"YouTubeFetch video, channel, and search data from YouTube
data fields (default)
Flow Control
"aggregator"AggregatorCollect and combine inputs
data fields (default)
"delay"DelayWait for specified duration
data fields (default)
"iterator"IteratorLoop through arrays or repeat
data fields (default)
"loopOverItems"Loop Over ItemsProcess items in batches with loops
data fields (default)
"parallel"ParallelExecute branches in parallel
data fields (default)
"subWorkflow"Sub-workflowExecute another workflow
data fields (default)
"waitApproval"Wait for ApprovalPause workflow execution until human approval is given
data fields (default)
Integrations
"elevenlabs"ElevenLabsText to speech with your ElevenLabs voices, transcribe audio (Scribe), generate sound effects, list voices
data fields (default)
"facebookAds"Facebook AdsRun Meta Marketing API ops: campaigns, ad sets, ads, creatives, audiences, lead forms, and insights
data fields (default)
"facebookFundraiser"Facebook FundraiserManage fundraisers, pull donations, sync to CRM, and analyze fundraiser performance
data fields (default)
"facebookGroups"Facebook GroupsList groups, publish to groups, and read members & feeds via the Facebook Groups API
data fields (default)
"facebookPages"Facebook PagesPublish posts, schedule content, reply to comments, and read Page insights on Facebook
data fields (default)
"heygen"HeyGenAI avatar videos from a script or audio, and video translation with lip-sync
data fields (default)
"instagram"InstagramPublish photos, reels & carousels, manage comments, send DMs, and read insights on Instagram
data fields (default)
"integration"IntegrationConnect to 300+ services and APIs with pre-built integrations
data fields (default)
"jira"JiraCreate, update, search, and manage Jira issues
"linkedin"LinkedInPublish posts (text, images, video or a link), comment, react, delete posts, and read the profile of your connected LinkedIn member
data fields (default)
"messenger"MessengerSend messages, list conversations, and read messages via the Facebook Messenger Platform
data fields (default)
"metaCatalog"Meta CatalogManage Meta commerce catalogs, products, and product sets across Facebook & Instagram
data fields (default)
"tavus"TavusPersonalized replica videos and live AI video conversations
data fields (default)
"threads"ThreadsPublish posts and carousels, reply, fetch threads/replies, and pull insights via the Threads API
data fields (default)
"tiktokAds"TikTok AdsCreate and manage TikTok ad campaigns, audiences, and reporting
"tiktokAdTools"TikTok Ad ToolsManage ad comments, blocked words, and automated optimization rules
"tiktokBrandSafety"TikTok Brand SafetyManage video/profile exclusion lists and inventory filters
"tiktokCommerce"TikTok CommerceManage TikTok stores, catalogs, products, and store windows
"tiktokCreative"TikTok CreativeManage TikTok creative assets, videos, images, templates, and discovery
"tiktokCreator"TikTok CreatorManage TikTok creator partnerships, campaigns, and video linking
"tiktokDPACatalog"TikTok DPA CatalogManage TikTok product catalogs, feeds, product sets, and video packages
"tiktokFinance"TikTok FinancePayment portfolios, credit lines, partner insights, and business plugin
"tiktokLeads"TikTok LeadsManage TikTok lead generation forms, fields, and lead data
"tiktokMeasurement"TikTok MeasurementApp/pixel event tracking, SDK config, deeplinks, and PPS surveys
"tiktokMentions"TikTok MentionsTrack brand mentions, hashtags, and comments on TikTok
"tiktokOfflineEvents"TikTok Offline EventsTrack and manage offline conversion events for TikTok campaigns
"tiktokPixel"TikTok PixelManage TikTok pixels, events, stats, and health reporting
"tiktokReporting"TikTok ReportingAd reporting, benchmarks, video performance, and Smart+ analytics
"tiktokRF"TikTok R&FManage TikTok Reach & Frequency campaigns, inventory, and contracts
"tiktokTCM"TikTok TCMTikTok Creator Marketplace for campaigns, creator discovery, and reporting
"twitter"X (Twitter)Post tweets, reply, search, manage lists, DMs, and more on X
data fields (default)
"whatsappBusiness"WhatsApp BusinessSend WhatsApp Business messages and approved templates via the Cloud API
data fields (default)
Triggers
"errorTrigger"Error TriggerTrigger on workflow errors
data fields (default)
"manualTrigger"Manual TriggerManual trigger with forms
data fields (default)
"scheduleTrigger"Schedule TriggerTrigger on schedule with cron
data fields (default)
"trigger"TriggerStart workflow with various triggers
data fields (default)
Worked examples
A scheduled digest
Runs on a cron schedule, scrapes a page, summarises it with an AI agent, and emails the result.
{
"version": "1.0",
"workflow": {
"name": "Daily Competitor Digest",
"description": "Scrape a competitor's blog every morning, summarise it, and email the result.",
"nodes": [
{
"id": "schedule",
"type": "scheduleTrigger",
"position": { "x": 250, "y": 50 },
"data": {
"label": "Every weekday at 09:00",
"cronExpression": "0 9 * * 1-5",
"timezone": "UTC"
}
},
{
"id": "scrape",
"type": "webscraper",
"position": { "x": 250, "y": 200 },
"data": {
"label": "Fetch the blog",
"url": "https://example.com/blog",
"scrapeType": "markdown",
"onlyMainContent": true
}
},
{
"id": "summarise",
"type": "aiAgent",
"position": { "x": 250, "y": 350 },
"data": {
"label": "Summarise the posts",
"model": "claude-3-5-haiku-20241022",
"systemPrompt": "Summarise the following posts as five bullet points. Be specific about product changes.",
"temperature": 0.3,
"maxTokens": 1024
}
},
{
"id": "email",
"type": "sendEmail",
"position": { "x": 250, "y": 500 },
"data": {
"label": "Send the digest",
"to": "team@example.com",
"subject": "Competitor digest",
"body": "{{$json.output}}"
}
}
],
"edges": [
{ "id": "e1", "source": "schedule", "target": "scrape" },
{ "id": "e2", "source": "scrape", "target": "summarise" },
{ "id": "e3", "source": "summarise", "target": "email" }
]
}
}Branching on a value
A condition node routing urgent messages one way and everything else another, with explicit handles.
{
"version": "1.0",
"workflow": {
"name": "Triage Incoming Message",
"nodes": [
{
"id": "start",
"type": "trigger",
"position": { "x": 250, "y": 50 },
"data": { "label": "When a message arrives", "triggerType": "message" }
},
{
"id": "is_urgent",
"type": "condition",
"position": { "x": 250, "y": 200 },
"data": {
"label": "Is it urgent?",
"variable": "{{$json.priority}}",
"operator": "equals",
"value": "urgent"
}
},
{
"id": "page_oncall",
"type": "sendEmail",
"position": { "x": 60, "y": 380 },
"data": {
"label": "Page on-call",
"to": "oncall@example.com",
"subject": "Urgent: {{$json.subject}}",
"body": "{{$json.body}}"
}
},
{
"id": "queue_normal",
"type": "action",
"position": { "x": 440, "y": 380 },
"data": {
"label": "Add to the normal queue",
"actionType": "queue"
}
}
],
"edges": [
{ "id": "e1", "source": "start", "target": "is_urgent" },
{ "id": "e2", "source": "is_urgent", "target": "page_oncall", "sourceHandle": "true" },
{ "id": "e3", "source": "is_urgent", "target": "queue_normal", "sourceHandle": "false" }
]
}
}Batching over a list
Fan a list out in batches, call an API per batch, and collect the results back into one array. Written in the terse bare-graph form.
{
"nodes": [
{ "id": "start", "type": "trigger" },
{ "id": "fanout", "type": "loopOverItems", "data": { "batchSize": 25 } },
{ "id": "call", "type": "graphql", "data": { "endpoint": "https://api.example.com/graphql" } },
{ "id": "collect","type": "aggregator", "data": { "aggregationType": "array" } }
],
"edges": [
{ "source": "start", "target": "fanout" },
{ "source": "fanout", "target": "call" },
{ "source": "call", "target": "collect" }
]
}Troubleshooting
| Field | Type | Required | Notes |
|---|---|---|---|
Unknown node type "…" | error | No | The type is not in the catalog. Types are camelCase and case-sensitive: aiAgent, not aiagent or AI Agent. |
Edge references unknown target node | error | No | The id in the edge does not match any node id. Often a rename that was applied to the node but not to the edges pointing at it. |
Duplicate node id | error | No | Two nodes share an id — usually from copying a block and forgetting to rename it. Edges cannot tell them apart. |
Invalid JSON: … | error | No | A syntax problem. Nearly always a trailing comma after the last item in an array or object; JSON does not allow them. The message carries the line and column. |
The connections form a loop: … | error | No | An edge points back at a node earlier in the flow. The message names the exact path. Replace the backwards edge with a loop node (loopOverItems, iterator). |
"workflow" must be an object | error | No | The workflow key is present but holds null, an array or a string. Give it a body, or drop it and use the bare shape. |
No trigger node | warning | No | Nothing will start the workflow. Add a trigger, scheduleTrigger, manualTrigger or errorTrigger. |
Nodes overlap on the canvas | — | No | Two nodes were given the same position. Either space them ~150px apart on y, or delete both positions and let them auto-place. |
Still stuck?
Build a small version on the canvas, open the JSON editor, and read the JSON it produces. Comparing working output against what you wrote is the fastest way to find the difference.
Open the builder