MultiverseAI Logo
    Workflow JSON

    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.

    Jump to quick start[ Format version 1.0 ]

    Overview

    Three ways to move JSON in and out of the builder — all of them use the format described on this page.

    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

    The shortest workflow the builder will accept. Paste this into the JSON editor and press Apply to canvas.
    {
      "nodes": [
        { "id": "start", "type": "trigger" },
        { "id": "reply", "type": "aiAgent" }
      ],
      "edges": [
        { "source": "start", "target": "reply" }
      ]
    }
    Two nodes and one connection. No positions, no data, no envelope — all of it is filled in for you.

    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

    The editor and the importer accept three shapes. They are equivalent — use whichever is convenient.

    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": [ /* … */ ]
      }
    }
    FieldTypeRequiredNotes
    versionstringNoFormat version. A different value still imports, with a warning telling you to re-export.
    exportedAtstringNoISO timestamp, written on export. Ignored on import.
    workflow.namestringNoDefaults to "Untitled Workflow". Applying JSON renames the open workflow.
    workflow.descriptionstringNoFree text. Defaults to empty.
    workflow.nodesNode[]YesThe steps. May be empty, but the key must be present.
    workflow.edgesEdge[]NoThe 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

    A node is one step. Its type decides what it does; its data configures it.
    {
      "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
      }
    }
    FieldTypeRequiredNotes
    idstringYesUnique within the document. Edges reference it, so pick something readable — "summarise" beats "node_3".
    typestringYesOne 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 }NoCanvas coordinates in pixels; x grows right, y grows down. Omit it and nodes are stacked 150px apart at x = 250.
    dataobjectNoType-specific configuration, merged over the type defaults. Anything you leave out keeps its default.
    data.labelstringNoThe name shown on the canvas. Defaults to the node type’s display name.
    style / hidden / parentId / extentvariousNoOptional 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

    An edge connects one node's output to another node's input. Output flows along it.
    {
      "id": "e2",
      "source": "scrape",
      "target": "summarise",
      "sourceHandle": null,
      "targetHandle": null,
      "type": "default"
    }
    FieldTypeRequiredNotes
    sourcestringYesThe id of the node the data comes from. Must exist in this document.
    targetstringYesThe id of the node the data goes to. Must exist in this document.
    idstringNoUnique within the document. Generated for you if omitted.
    sourceHandlestring | nullNoWhich output of the source node to leave from. Only meaningful for branching nodes.
    targetHandlestring | nullNoWhich input of the target node to arrive at. Almost always omitted.
    typestringNoEdge 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

    Most nodes have one output. Branching nodes have several, and sourceHandle picks which one an edge leaves from.
    Node typeValid sourceHandleNotes
    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" }
      ]
    }
    Routing the two outputs of a condition node.

    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

    Any string in data can pull values from the running workflow using {{ … }}.
    {
      "id": "email",
      "type": "sendEmail",
      "data": {
        "to": "{{$json.customer.email}}",
        "subject": "Your order {{$json.orderId}}",
        "body": "Processed at {{$now}} by workflow {{$workflow.name}}."
      }
    }
    FieldTypeRequiredNotes
    {{$json.field}}anyNoA field from the current item coming into this node. The one you will use most.
    {{$items}} / {{$item(i)}}array / objectNoAll incoming items, or the item at index i.
    {{$itemIndex}}numberNoIndex of the item currently being processed.
    {{$node["id"].json.field}}anyNoOutput of any earlier node by id — not just the one directly upstream.
    {{$workflow.id}} / {{$workflow.name}}stringNoThe running workflow.
    {{$execution.id}} / {{$execution.mode}}stringNoThis run. Mode is manual, trigger, webhook or integrated.
    {{$now}} / {{$today}}DateNoCurrent 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

    The editor validates as you type. Errors block Apply; warnings tell you what was filled in on your behalf.

    Errors — these must be fixed

    • The document is not valid JSON, or the top level is not an object.
    • nodes is missing, or nodes/edges is 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 position is present but its x/y are not numbers.
    • data is 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 sourceHandle values 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 sourceHandle on 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 version that 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

    Every value 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.
    92 types

    Actions

    "action"Action

    Perform various actions

    data fields (default)

    actionType = "message"messageTemplate = ""

    AI & Logic

    "aiAgent"AI Agent

    AI-powered processing

    data fields (default)

    model = "google/gemini-3-flash-preview"systemPrompt = ""temperature = 0.7maxTokens = 2048
    "condition"Condition

    If/then logic

    data fields (default)

    variable = ""operator = "equals"value = ""
    "filter"Filter

    Filter data based on conditions

    data fields (default)

    filterType = "condition"condition = ""invertResult = false
    "switch"Switch

    Multi-branch routing logic

    data fields (default)

    mode = "rules"rules = []outputs = 3fallbackOutput = 0

    AI/ML Enhancement

    "aiEmbeddings"AI Embeddings

    Convert text into high-dimensional vector embeddings

    data fields (default)

    model = "text-embedding-3-small"
    "aiTransform"AI Transform

    AI-powered data transformation and processing

    data fields (default)

    model = "google/gemini-3-flash-preview"instruction = ""
    "contentSummarization"Content Summarization

    Generate intelligent summaries from long-form content

    data fields (default)

    model = "google/gemini-3-flash-preview"maxLength = 200
    "multiAIProvider"Multi-AI Provider

    Orchestrate multiple AI providers for improved reliability

    data fields (default)

    providers = []strategy = "fallback"
    "semanticSearch"Semantic Search

    Intelligent semantic search with vector similarity

    data fields (default)

    query = ""topK = 5
    "vectorDatabase"Vector Database

    Store, search, and manage high-dimensional embeddings

    data fields (default)

    operation = "query"topK = 5

    Canvas

    "stickyNote"stickyNote

    Added from the canvas, not the node palette.

    data fields (default)

    text = "Add a note..."color = "yellow"

    Communication

    "angelsend"AngelSend

    Run any AngelSend email-outreach action: campaigns, steps, pause/resume/stop one recipient, merge vars, sender limits, opt-outs, webhooks

    data fields (default)

    action = "list-campaigns"paramsJson = "{}"
    "chatInterface"Chat Interface

    Send to Slack, Discord, Teams, Google Chat, Mattermost or Telegram

    data fields (default)

    platform = "slack"action = "send"channel = ""message = ""
    "ftp"FTP/SFTP

    Transfer files via FTP, SFTP, or FTPS

    data fields (default)

    host = ""username = ""password = ""operation = "list"
    "graphql"GraphQL

    Execute GraphQL queries, mutations, and subscriptions

    data fields (default)

    endpoint = ""query = ""variables = {}
    "meetingNotetaker"Meeting Notetaker

    Send 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)

    operation = "getMeeting"meetingId = "{{trigger.meeting_id}}"
    "rssFeed"RSS Feed

    Fetch and process RSS/Atom feeds

    data fields (default)

    feedUrl = ""maxItems = 20
    "sendEmail"Send Email

    Send emails via SMTP, Gmail, or other providers

    data fields (default)

    to = ""subject = ""body = ""
    "slack"Slack

    Post messages to Slack via your connected workspace or an incoming webhook

    data fields (default)

    message = ""channel = ""webhookUrl = ""
    "ssh"SSH Command

    Execute commands on remote servers via SSH

    data fields (default)

    host = ""username = ""command = ""
    "webhookResponse"Webhook Response

    Send custom HTTP responses back to webhook triggers

    data fields (default)

    statusCode = 200body = ""headers = {"Content-Type":"application/json"}

    Data Processing

    "codeExecution"Code Execution

    Execute custom JavaScript code

    data fields (default)

    language = "javascript"code = "// return value becomes node output\nreturn in…
    "compareDatasets"Compare Datasets

    Compare two datasets and find differences

    data fields (default)

    compareKey = ""includeOnlyLeft = trueincludeOnlyRight = trueincludeBoth = true
    "convertToFile"Convert to File

    Convert data to various file formats

    data fields (default)

    targetType = "csv"filename = "output.csv"
    "crypto"Crypto

    Encryption, hashing, and cryptographic operations

    data fields (default)

    operation = "hash"algorithm = "sha256"
    "databaseQuery"Database Query

    Execute SQL queries with a visual editor and result preview

    data fields (default)

    connection = ""query = "SELECT 1;"
    "dateTime"Date & Time

    Date and time operations

    data fields (default)

    operation = "now"format = "YYYY-MM-DD HH:mm:ss"
    "extractFromFile"Extract From File

    Extract data from PDF, Excel, CSV files

    data fields (default)

    fileType = "pdf"source = ""
    "jsonOperations"JSON Operations

    Parse, transform, and manipulate JSON data

    data fields (default)

    operation = "parse"path = ""
    "removeDuplicates"Remove Duplicates

    Remove duplicate items from datasets

    data fields (default)

    keyField = ""scope = "all"

    Data Sources

    "gmail"Gmail

    Read, search, send, and manage emails via Gmail API

    data fields (default)

    operation = "list"maxResults = 25
    "googleCalendar"Google Calendar

    Create, list and update events, add Meet links, check availability

    data fields (default)

    operation = "createEvent"
    "googleChat"Google Chat

    Post and manage messages in Google Chat spaces as you

    data fields (default)

    operation = "sendMessage"
    "googleContacts"Google Contacts

    Look up, add and update Google Contacts

    data fields (default)

    operation = "search"
    "googleDocs"Google Docs

    Create, read, and export Google Docs documents

    data fields (default)

    operation = "get"
    "googleDrive"Google Drive

    List, upload, download, and manage files in Google Drive

    data fields (default)

    operation = "list"
    "googleForms"Google Forms

    Build forms and read their responses

    data fields (default)

    operation = "listResponses"
    "googleMeet"Google Meet

    Create Google Meet links

    data fields (default)

    operation = "createSpace"
    "googlePhotos"Google Photos

    Browse albums and media items from Google Photos

    data fields (default)

    operation = "list"
    "googleSearch"Google Search

    Raw Google results (web, news or images) via Serper

    data fields (default)

    query = ""searchType = "general"numResults = 5
    "googleSheets"Google Sheets

    Read, write, append and look up rows in Google Sheets

    data fields (default)

    operation = "read"
    "googleSlides"Google Slides

    Create decks, add slides and fill {{placeholders}} in Google Slides

    data fields (default)

    operation = "create"
    "googleTasks"Google Tasks

    Create, list and complete Google Tasks

    data fields (default)

    operation = "createTask"
    "promptInput"Prompt Input

    Use saved prompt and response as input

    data fields (default)

    selectedPromptId = nullselectedPrompt = null
    "tiktok"TikTok

    Manage TikTok account, videos, and engagement

    data fields (default)

    operation = "getProfile"
    "webscraper"Web Scraper

    Extract content from websites using Firecrawl

    data fields (default)

    url = ""scrapeType = "markdown"onlyMainContent = truewaitFor = —
    "websearch"Web Search

    AI-powered web search with Perplexity

    data fields (default)

    query = ""model = "sonar"systemPrompt = ""searchRecencyFilter = ""domainFilter = ""maxTokens = —
    "youtube"YouTube

    Fetch video, channel, and search data from YouTube

    data fields (default)

    operation = "search"limit = 10

    Flow Control

    "aggregator"Aggregator

    Collect and combine inputs

    data fields (default)

    aggregationType = "array"waitTimeout = 30
    "delay"Delay

    Wait for specified duration

    data fields (default)

    delayType = "fixed"delaySeconds = 1
    "iterator"Iterator

    Loop through arrays or repeat

    data fields (default)

    maxIterations = 10itemsSource = "input"
    "loopOverItems"Loop Over Items

    Process items in batches with loops

    data fields (default)

    batchSize = 10itemsSource = "input"
    "parallel"Parallel

    Execute branches in parallel

    data fields (default)

    branches = [{"id":"branch-0","label":"Branch 1","status":"…maxConcurrency = 2waitForAll = truefailFast = false
    "subWorkflow"Sub-workflow

    Execute another workflow

    data fields (default)

    workflowId = ""
    "waitApproval"Wait for Approval

    Pause workflow execution until human approval is given

    data fields (default)

    message = "Please review and approve."timeout = 3600

    Integrations

    "elevenlabs"ElevenLabs

    Text to speech with your ElevenLabs voices, transcribe audio (Scribe), generate sound effects, list voices

    data fields (default)

    operation = "textToSpeech"text = ""modelId = "eleven_multilingual_v2"
    "facebookAds"Facebook Ads

    Run Meta Marketing API ops: campaigns, ad sets, ads, creatives, audiences, lead forms, and insights

    data fields (default)

    operation = "get-ad-accounts"limit = 25
    "facebookFundraiser"Facebook Fundraiser

    Manage fundraisers, pull donations, sync to CRM, and analyze fundraiser performance

    data fields (default)

    operation = "get-fundraisers"limit = 50
    "facebookGroups"Facebook Groups

    List groups, publish to groups, and read members & feeds via the Facebook Groups API

    data fields (default)

    operation = "facebookGroupGetGroups"limit = 25
    "facebookPages"Facebook Pages

    Publish posts, schedule content, reply to comments, and read Page insights on Facebook

    data fields (default)

    operation = "facebookPageGetPages"limit = 25
    "heygen"HeyGen

    AI avatar videos from a script or audio, and video translation with lip-sync

    data fields (default)

    operation = "heygenCreateVideo"aspectRatio = "16:9"resolution = "720p"engine = "avatar_iv"waitForCompletion = truemaxWaitSeconds = 600
    "instagram"Instagram

    Publish photos, reels & carousels, manage comments, send DMs, and read insights on Instagram

    data fields (default)

    operation = "instagramGetMedia"limit = 25
    "integration"Integration

    Connect to 300+ services and APIs with pre-built integrations

    data fields (default)

    provider = ""service = ""operation = ""authentication = {"type":"api-key","configured":false}executionStatus = "idle"
    "jira"Jira

    Create, update, search, and manage Jira issues

    "linkedin"LinkedIn

    Publish posts (text, images, video or a link), comment, react, delete posts, and read the profile of your connected LinkedIn member

    data fields (default)

    operation = "linkedinPublishPost"text = ""visibility = "PUBLIC"attachment = "NONE"
    "messenger"Messenger

    Send messages, list conversations, and read messages via the Facebook Messenger Platform

    data fields (default)

    operation = "messengerSendMessage"
    "metaCatalog"Meta Catalog

    Manage Meta commerce catalogs, products, and product sets across Facebook & Instagram

    data fields (default)

    operation = "metaGetCatalogs"limit = 25
    "tavus"Tavus

    Personalized replica videos and live AI video conversations

    data fields (default)

    operation = "tavusCreateVideo"waitForCompletion = truemaxWaitSeconds = 600maxMinutes = 10
    "threads"Threads

    Publish posts and carousels, reply, fetch threads/replies, and pull insights via the Threads API

    data fields (default)

    operation = "threadsGetPosts"limit = 25
    "tiktokAds"TikTok Ads

    Create and manage TikTok ad campaigns, audiences, and reporting

    "tiktokAdTools"TikTok Ad Tools

    Manage ad comments, blocked words, and automated optimization rules

    "tiktokBrandSafety"TikTok Brand Safety

    Manage video/profile exclusion lists and inventory filters

    "tiktokCommerce"TikTok Commerce

    Manage TikTok stores, catalogs, products, and store windows

    "tiktokCreative"TikTok Creative

    Manage TikTok creative assets, videos, images, templates, and discovery

    "tiktokCreator"TikTok Creator

    Manage TikTok creator partnerships, campaigns, and video linking

    "tiktokDPACatalog"TikTok DPA Catalog

    Manage TikTok product catalogs, feeds, product sets, and video packages

    "tiktokFinance"TikTok Finance

    Payment portfolios, credit lines, partner insights, and business plugin

    "tiktokLeads"TikTok Leads

    Manage TikTok lead generation forms, fields, and lead data

    "tiktokMeasurement"TikTok Measurement

    App/pixel event tracking, SDK config, deeplinks, and PPS surveys

    "tiktokMentions"TikTok Mentions

    Track brand mentions, hashtags, and comments on TikTok

    "tiktokOfflineEvents"TikTok Offline Events

    Track and manage offline conversion events for TikTok campaigns

    "tiktokPixel"TikTok Pixel

    Manage TikTok pixels, events, stats, and health reporting

    "tiktokReporting"TikTok Reporting

    Ad reporting, benchmarks, video performance, and Smart+ analytics

    "tiktokRF"TikTok R&F

    Manage TikTok Reach & Frequency campaigns, inventory, and contracts

    "tiktokTCM"TikTok TCM

    TikTok 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)

    operation = "post"
    "whatsappBusiness"WhatsApp Business

    Send WhatsApp Business messages and approved templates via the Cloud API

    data fields (default)

    operation = "whatsappSendMessage"

    Triggers

    "errorTrigger"Error Trigger

    Trigger on workflow errors

    data fields (default)

    catchAll = true
    "manualTrigger"Manual Trigger

    Manual trigger with forms

    data fields (default)

    formFields = []
    "scheduleTrigger"Schedule Trigger

    Trigger on schedule with cron

    data fields (default)

    cronExpression = "0 9 * * *"timezone = "UTC"
    "trigger"Trigger

    Start workflow with various triggers

    data fields (default)

    triggerType = "test"

    Worked examples

    Complete, valid documents you can paste in as-is.

    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

    The errors people hit most, and what they actually mean.
    FieldTypeRequiredNotes
    Unknown node type "…"errorNoThe type is not in the catalog. Types are camelCase and case-sensitive: aiAgent, not aiagent or AI Agent.
    Edge references unknown target nodeerrorNoThe 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 iderrorNoTwo nodes share an id — usually from copying a block and forgetting to rename it. Edges cannot tell them apart.
    Invalid JSON: …errorNoA 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: …errorNoAn 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 objecterrorNoThe 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 nodewarningNoNothing will start the workflow. Add a trigger, scheduleTrigger, manualTrigger or errorTrigger.
    Nodes overlap on the canvas—NoTwo 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