nflow — A compact language for n8n workflows¶
nflow compiles human-readable .nflow files into valid n8n workflow JSON. Describe your automations in ~50 lines instead of ~5,000.
Quick Start¶
WORKFLOW "My First Automation" active
CREDENTIAL @myapi = httpHeaderAuth "My API Key"
TRIGGER webhook AS "Incoming Hook" { path: "/hook", method: POST }
SET "Config" { baseUrl: "https://api.example.com" } +passthrough
HTTP POST {{ $json.baseUrl }}/items @myapi AS "Create Item" {
jsonBody: {{ JSON.stringify($json) }}
}
IF "Success?" { conditions: AND [{{ $json.status }} equals "ok"] }
// Connections
"Incoming Hook" -> "Config" -> "Create Item" -> "Success?"
"Success?" -> TRUE -> ...
"Success?" -> FALSE -> ...
1. Workflow & Credentials¶
WORKFLOW "Name" [active]
CREDENTIAL @alias = credentialType "Display Name"
// e.g.
CREDENTIAL @myapi = httpHeaderAuth "My API Key"
CREDENTIAL @bearer = httpBearerAuth "Bearer Auth Token"
CREDENTIAL @gsheets = googleSheetsOAuth2Api "Google Sheets Account"
When compiling with -o, a separate <name>-credentials.json file is generated alongside the workflow JSON. Credential IDs are deterministic (derived from the credential name), so re-compiling always produces the same IDs.
Import order: import the credentials file first, then the workflow.
Linking existing n8n credentials¶
If you already have credentials in n8n, export them and pass the file with -c:
The compiler matches CREDENTIAL declarations by name against the exported file and uses the real n8n IDs. No credentials file is generated for linked credentials — just import the workflow directly.
Deploying to n8n (Docker)¶
Use scripts/n8n-sync.sh to export/import credentials and workflows. Set N8N_HOST for a remote VPS:
# Export credentials from remote VPS
N8N_HOST=root@my-vps.com ./scripts/n8n-sync.sh export-creds credentials.json
# Compile with linked credentials, then deploy
nflow api.nflow -c credentials.json -o api.json
N8N_HOST=root@my-vps.com ./scripts/n8n-sync.sh deploy api.json
# Or compile without linking (generates api-credentials.json), deploy both
nflow api.nflow -o api.json
N8N_HOST=root@my-vps.com ./scripts/n8n-sync.sh deploy api.json api-credentials.json
| Variable | Default | Purpose |
|---|---|---|
N8N_HOST |
(empty = local Docker) | SSH destination, e.g. root@my-vps.com |
N8N_CONTAINER |
n8n-n8n-1 |
Docker container name |
2. Node Types¶
TRIGGER — Entry points¶
TRIGGER manual AS "Run Manually"
TRIGGER gsheets_update AS "Sheet Trigger" {
doc: "https://docs.google.com/spreadsheets/d/.../edit",
sheet: "Sheet1",
event: rowUpdate,
watch: ["trigger"],
poll: everyMinute
}
TRIGGER webhook AS "Webhook" { path: "/my-hook", method: POST }
TRIGGER webhook AS "Streaming Hook" { method: POST, responseMode: streaming }
TRIGGER webhook AS "Last Node" { responseMode: lastNode, options: { rawBody: true } }
TRIGGER cron AS "Schedule" { expression: "0 * * * *" }
TRIGGER form @basicauth AS "My Form" {
formTitle: "Contact Us",
formDescription: "Fill out this form",
authentication: basicAuth,
formFields: { values: [
{},
{ fieldType: "email" },
{ fieldType: "number" }
]}
}
SET — Assign variables¶
SET "Config" { apiUrl: "https://...", sheetName: "data" } +passthrough
SET "Extract ID" { id: {{ $json.event.id }} }
+passthrough keeps all upstream fields (like n8n's "Include Other Fields").
HTTP — API requests¶
// Simple
HTTP GET https://api.example.com/items @myauth AS "Get Items"
// With body, query, headers
HTTP POST https://api.example.com/items @myauth AS "Create" onError:continue {
body: { name: {{ $json.name }}, type: "default" },
query: { page: 1 },
headers: { "X-Custom": "value" }
}
// Raw JSON body
HTTP POST https://api.example.com/items @myauth AS "Create" {
jsonBody: {{ JSON.stringify($json) }}
}
// Download a file as binary
HTTP GET https://example.com/report.pdf AS "Download PDF" {
options: {
response: { response: { responseFormat: "file", outputPropertyName: "data" } }
}
}
// Upload binary data
HTTP POST https://example.com/upload @myauth AS "Upload File" {
sendBody: true,
contentType: "binaryData",
inputDataFieldName: "data"
}
CODE — JavaScript¶
// Inline (single backticks)
CODE "Transform" `return $input.all().map(i => ({json: i.json.data}));`
// Multi-line (triple backticks)
CODE "Complex Logic" ```
const items = $input.all();
for (const item of items) {
item.json.processed = true;
}
return items;
// Python code
CODE "Process" python return [{"json": {"ok": True}} for item in items]
// Run once per item (instead of once for all items)
CODE "Per Item" +each return [{json: {processed: true, ...item.json}}];
// Python + per-item combined CODE "Transform Each" python +each ``` item = items[0] item["json"]["done"] = True return [item]
**Options:** `python` keyword selects Python language (default: JavaScript). `+each` flag sets mode to "Run Once for Each Item" (default: "Run Once for All Items").
### FILTER — Drop non-matching items (single output)
```nflow
FILTER "Active Only" { conditions: AND [
{{ $json.status }} equals "active",
{{ $json.id }} notEmpty
]}
IF — Conditional branch (TRUE / FALSE outputs)¶
IF "Is Admin?" { conditions: AND [{{ $json.role }} equals "admin"] }
IF "Has Data?" { conditions: OR [{{ $json.items }} arrayNotEmpty] }
Available operators: equals, notEquals, contains, notContains, startsWith, endsWith, empty, notEmpty, exists, notExists, gt, gte, lt, lte, numEquals, isTrue, isFalse, regex, arrayEmpty, arrayNotEmpty
SWITCH — N-way conditional routing¶
// Each rule is a routing output; reuses IF/FILTER condition syntax
SWITCH "Route By Type" { rules: [
AND [{{ $json.type }} equals "email"],
AND [{{ $json.type }} equals "sms"],
AND [{{ $json.type }} equals "push"]
]}
// Rules with multiple conditions per branch
SWITCH "Complex Route" { rules: [
AND [{{ $json.type }} equals "email", {{ $json.priority }} gt 5],
OR [{{ $json.active }} isTrue, {{ $json.admin }} isTrue]
], looseTypeValidation: true, options: { ignoreCase: true } }
// Connect outputs by index
"Route By Type" -> 0 -> "Handle Email"
"Route By Type" -> 1 -> "Handle SMS"
"Route By Type" -> 2 -> "Handle Push"
MERGE — Combine branches¶
MERGE "Combine All" { mode: combine, by: position, inputs: 3 }
MERGE "Pick Branch" { mode: chooseBranch, useInput: 2 }
MERGE "Append" { mode: append }
DATETIME — Date & time operations¶
DATETIME "Extract Week" { operation: extractDate, part: week }
DATETIME "Format Date" { operation: formatDate, date: {{ $json.created }}, format: "MM/DD/YYYY" }
DATETIME "Current Date" { operation: getCurrentDate, includeCurrentTime: true }
DATETIME "Time Between" {
operation: getTimeBetweenDates,
startDate: {{ $json.start }},
endDate: {{ $json.end }},
units: ["day", "hour"]
}
DATETIME "Add 7 Days" { operation: addToDate, duration: 7, timeUnit: days }
Operations: addToDate, extractDate, formatDate, getCurrentDate, getTimeBetweenDates, roundDate, subtractFromDate
LIMIT — Keep first/last N items¶
LOOP — Loop Over Items (Split in Batches)¶
LOOP "Process Items" { batchSize: 1 }
LOOP "Batch of 10" { batchSize: 10 }
// Output 0 = done (all processed), Output 1 = loop body (current batch)
"Process Items" -> LOOP -> "Do Work"
"Do Work" -> "Process Items"
"Process Items" -> DONE -> "All Finished"
GSHEET — Google Sheets¶
GSHEET READ @gsheets AS "Get Rows" {
doc: "https://docs.google.com/.../edit",
sheet: "Sheet1"
}
GSHEET UPDATE @gsheets AS "Write Status" {
doc: {{ $('Config').item.json.DocUrl }},
sheet: {{ $('Config').item.json.SheetName }},
match: ["ID"],
values: { ID: {{ $json.ID }}, status: "done" }
}
GDRIVE — Google Drive¶
NODE — Generic node (any n8n node type)¶
The NODE keyword lets you use any of the 500+ n8n nodes, even those without a dedicated DSL keyword. The compiler looks up the node type in the registry to resolve the correct version and serialize parameters.
// Postgres query
NODE "n8n-nodes-base.postgres" @pg AS "Query DB" {
operation: "executeQuery",
query: "SELECT * FROM users WHERE active = true"
}
// Redis cache
NODE "n8n-nodes-base.redis" @redis AS "Cache Result" {
operation: "set",
key: "users_cache",
value: {{ JSON.stringify($json) }},
expire: true,
ttl: 3600
}
// Respond to webhook
NODE "n8n-nodes-base.respondToWebhook" AS "Respond" {
respondWith: "json",
responseBody: {{ JSON.stringify($json) }}
}
// Slack message
NODE "n8n-nodes-base.slack" @slack AS "Send Alert" {
resource: "message",
operation: "post",
channel: "#alerts",
text: {{ "Alert: " + $json.message }}
}
// Google Calendar event
NODE "n8n-nodes-base.googleCalendar" @gcal AS "Create Event" {
resource: "event",
operation: "create",
calendarId: "primary",
start: "2025-01-01T10:00:00",
end: "2025-01-01T11:00:00"
}
// Short form: if no dot in type name, "n8n-nodes-base." is assumed
NODE "redis" @redis AS "Get Key" { operation: "get", key: "mykey" }
// All standard settings work: credentials, flags, options
NODE "n8n-nodes-base.httpRequest" @api AS "Fetch" +retry onError:continue {
method: "GET",
url: "https://api.example.com/data",
options: { timeout: 5000 }
}
The NODE keyword supports all standard features: @credential references, AS "Name", flags (+once, +retry, onError:, disabled, notes:), and options: { } blocks.
When to use NODE vs ergonomic keywords: Use HTTP, GSHEET, TRIGGER, etc. for their concise syntax when available. Use NODE for any n8n node not covered by a dedicated keyword (Postgres, Redis, Slack, Notion, Jira, Airtable, etc.).
Full parameter reference for all 547 nodes: See NODE-CATALOG.md for the compact index, or open nodes/ for detailed parameters and examples.
NOOP — Passthrough / convergence point¶
NOTE — Sticky note (visual only)¶
NOTE "Reminder" { content: "This section handles tickets", color: 4 }
NOTE "Large Note" { content: "Architecture overview", height: 400, width: 600 }
Default dimensions: height: 160, width: 240. Colors: 1–7.
2b. AI Agent Nodes¶
nflow has first-class support for n8n's AI/LangChain nodes.
AGENT — The AI agent hub¶
LLM — Language model provider¶
LLM gemini @gemini AS "Gemini" { model: "models/gemini-2.5-flash", temperature: 0 }
LLM openai @openai AS "GPT-4" { model: "gpt-4.1-mini", temperature: 0 }
LLM anthropic @claude AS "Claude" { model: "claude-sonnet-4-20250514", temperature: 0 }
LLM ollama @ollama AS "Local LLM" { model: "llama3" }
// Disable a node (present but inactive)
LLM openai AS "Backup LLM" disabled { model: "gpt-4.1-mini" }
MEMORY — Conversation memory¶
MEMORY buffer AS "Chat Memory" { contextWindowLength: 30 }
// With custom session key (for multi-session support)
MEMORY buffer AS "Session Memory" {
contextWindowLength: 20,
sessionIdType: "customKey",
sessionKey: "my_session_id"
}
All block parameters are passed through to n8n. See the Memory Buffer Window node reference for all available parameters.
TOOL — Agent tools / superpowers¶
// HTTP tool (fetch external API)
TOOL http AS "get_a_joke" {
url: "https://jokeapi.dev/joke/Any",
description: "Gets a random joke.",
optimizeResponse: true,
fields: "joke"
}
// Wikipedia lookup
TOOL wikipedia AS "wikipedia"
// Code tool (custom JS logic)
TOOL code AS "calculate_loan" {
description: "Calculates monthly loan payment.",
schemaExample: '{"amount": 250000, "rate": 6.5, "years": 30}',
code: ```
const monthly = query.amount * (query.rate / 1200);
return JSON.stringify({ payment: monthly.toFixed(2) });
// RSS feed reader TOOL rss AS "blog_feed" { url: "https://example.com/rss", description: "Gets latest blog posts." }
// Crypto / password generator TOOL crypto AS "make_password" { action: "generate", description: "Generate a secure password.", encodingType: "base64" }
// Date/time calculator TOOL datetime AS "days_until" { operation: "getTimeBetweenDates", description: "Days between now and a date." }
### TRIGGER chat — Chat interface trigger
```nflow
TRIGGER chat AS "Chat Window" {
public: true,
title: "My AI Assistant",
subtitle: "Ask me anything!",
initialMessages: "Hi there! 👋",
responseMode: "lastNode"
}
3. Connections¶
// Sequential
"A" -> "B" -> "C"
// Branching (IF nodes or onError)
"Check" -> TRUE -> "Yes Path"
"Check" -> FALSE -> "No Path"
"API Call" -> OK -> "Handle Response"
"API Call" -> ERR -> "Handle Error"
// Parallel fan-out (one source → multiple targets)
"Source" -> "Target A", "Target B", "Target C"
// Merge input targeting (which input slot)
"Branch A" -> "Merge":0
"Branch B" -> "Merge":1
"Branch C" -> "Merge":2
// Numeric output routing (for Switch and other N-output nodes)
"Switch" -> 0 -> "First Branch"
"Switch" -> 1 -> "Second Branch"
"Switch" -> 2 -> "Third Branch"
// Loop routing (DONE = all processed, LOOP = current batch)
"Loop" -> DONE -> "All Finished"
"Loop" -> LOOP -> "Process Item"
"Process Item" -> "Loop"
// AI connections (LLM, memory, tools wired to an agent)
"Gemini" -> LLM -> "My Agent"
"Chat Memory" -> MEMORY -> "My Agent"
"get_a_joke" -> TOOL -> "My Agent"
"wikipedia" -> TOOL -> "My Agent"
AI routing keywords: LLM (ai_languageModel), TOOL (ai_tool), MEMORY (ai_memory), OUTPUT_PARSER, RETRIEVER, EMBEDDING, DOCUMENT, TEXT_SPLITTER, VECTOR_STORE
4. Node Options¶
Any node that supports n8n options can include an options: { ... } block. Options are passed through directly to the n8n node's parameters.options object.
// HTTP with timeout and proxy
HTTP POST https://api.example.com @myapi AS "Create" {
body: { name: "test" },
options: { timeout: 10000, proxy: "http://myproxy:3821", allowUnauthorizedCerts: true }
}
// Google Sheets with locale
GSHEET READ @gsheets AS "Get Rows" {
doc: "https://docs.google.com/.../edit",
sheet: "Sheet1",
options: { locale: "en", autoRecalc: "ON_CHANGE" }
}
// Merge with reset
MERGE "Batch" { mode: append, options: { reset: true } }
// Agent with extra options alongside systemMessage
AGENT "Bot" { systemMessage: "You are helpful", options: { maxIterations: 10 } }
// SET with dotNotation
SET "Config" { apiUrl: "https://..." } +passthrough
// options: { dotNotation: true } can be added to any SET block
Shorthand keys like systemMessage (AGENT), temperature (LLM), and title/subtitle (TRIGGER chat) continue to work. If the same key appears in both the shorthand and options:, the options: value takes precedence.
5. Node Settings¶
Any node can carry inline settings that control n8n execution behavior. Boolean settings use a + prefix; value settings use key:value or key:"value" syntax.
// Execute once, always output data, retry on fail
HTTP POST https://api.example.com @myapi AS "Create" +once +always +retry {
body: { name: "test" }
}
// Retry with explicit max-tries and wait
HTTP GET https://api.example.com @myapi AS "Fetch" retry:5 wait:2000
// Error handling variants
HTTP GET https://api.example.com AS "Safe Call" onError:continue // → continueErrorOutput
HTTP GET https://api.example.com AS "Keep Going" onError:output // → continueRegularOutput
HTTP GET https://api.example.com AS "Strict" onError:stop // → stopWorkflow
// Inline notes (displayed on the canvas)
CODE "Transform" +once notes:"Normalize upstream payload" `return $input.all();`
// Combine everything
HTTP POST https://api.example.com @myapi AS "Critical" +once +always retry:3 wait:1000 onError:continue notes:"Auth required" {
jsonBody: {{ JSON.stringify($json) }}
}
| Setting | Syntax | n8n JSON output |
|---|---|---|
| Execute once | +once |
executeOnce: true |
| Always output data | +always |
alwaysOutputData: true |
| Retry on fail | +retry |
retryOnFail: true |
| Retry with max tries | retry:N |
retryOnFail: true, maxTries: N |
| Wait between retries | wait:N |
waitBetweenTries: N (ms) |
| Error → error output | onError:continue |
onError: "continueErrorOutput" |
| Error → regular output | onError:output |
onError: "continueRegularOutput" |
| Error → stop workflow | onError:stop |
onError: "stopWorkflow" |
| Inline notes | notes:"text" |
notes: "text", notesInFlow: true |
| Disable node | disabled |
disabled: true |
6. Layout (optional)¶
Auto-calculated if omitted.
7. Comments¶
Reference: Full Grammar¶
| Element | Syntax | Purpose |
|---|---|---|
| Workflow | ||
WORKFLOW |
WORKFLOW "Name" [active] |
Declare workflow |
CREDENTIAL |
CREDENTIAL @alias = type "Name" |
Reusable auth |
| Triggers | ||
TRIGGER |
TRIGGER type AS "Name" { ... } |
Entry point (manual, webhook, cron, gsheets_update, chat, form) |
| Data Nodes | ||
SET |
SET "Name" { k: v } [+passthrough] |
Assign variables |
HTTP |
HTTP METHOD url @cred AS "Name" { ... } |
API request |
CODE |
CODE "Name" [python] [+each] \...`` |
Code transform (JS/Python) |
FILTER |
FILTER "Name" { conditions: ... } |
Keep matching items |
IF |
IF "Name" { conditions: ... } |
Branch TRUE/FALSE |
SWITCH |
SWITCH "Name" { rules: [...] } |
N-way conditional routing |
MERGE |
MERGE "Name" { mode: ... } |
Combine branches |
DATETIME |
DATETIME "Name" { operation: ... } |
Date & time operations |
LIMIT |
LIMIT "Name" { maxItems: N } |
Keep first/last N items |
LOOP |
LOOP "Name" { batchSize: N } |
Loop over items in batches |
GSHEET |
GSHEET OP @cred AS "Name" { ... } |
Google Sheets |
GDRIVE |
GDRIVE OP @cred AS "Name" { ... } |
Google Drive |
NODE |
NODE "type" @cred AS "Name" { ... } |
Any n8n node (generic, 500+ supported) |
NOOP |
NOOP "Name" |
Passthrough |
NOTE |
NOTE "Name" { content: ... } |
Sticky note |
| AI Agent Nodes | ||
AGENT |
AGENT "Name" { systemMessage: "..." } |
AI agent hub |
LLM |
LLM provider @cred AS "Name" { ... } |
Language model (gemini, openai, anthropic, ollama) |
MEMORY |
MEMORY type AS "Name" { ... } |
Conversation memory (buffer, postgres, redis) |
TOOL |
TOOL type AS "Name" { ... } |
Agent tool (http, code, wikipedia, rss, crypto, datetime) |
| Connections | ||
-> |
"A" -> "B" |
Connect nodes |
-> TRUE/FALSE -> |
"If" -> TRUE -> "B" |
Branch output |
-> OK/ERR -> |
"Http" -> ERR -> "B" |
Error routing |
-> N -> |
"Switch" -> 0 -> "B" |
Numeric output index |
-> DONE/LOOP -> |
"Loop" -> DONE -> "B" |
Loop routing (done/body) |
-> LLM/TOOL/MEMORY -> |
"Gemini" -> LLM -> "Agent" |
AI connection |
"Node":N |
"A" -> "Merge":1 |
Target input slot |
| Node Settings | ||
+once |
HTTP GET url +once |
Execute once per run |
+always |
SET "X" +always { ... } |
Always output data |
+retry / retry:N |
HTTP GET url +retry or retry:5 |
Retry on fail (optional max tries) |
wait:N |
HTTP GET url retry:3 wait:2000 |
Wait between retries (ms) |
onError:X |
onError:continue / output / stop |
Error handling mode |
notes:"…" |
notes:"Check auth" |
Inline note (shown on canvas) |
disabled |
LLM openai AS "X" disabled { ... } |
Disable a node |
| Other | ||
options |
options: { key: val } |
n8n node options (any node) |
{{ expr }} |
{{ $json.field }} |
n8n expression |
// |
// comment |
Comment |
8. Node Registry¶
nflow includes a node registry (node-registry.json) that contains metadata for all 547 built-in n8n nodes. This enables the generic NODE keyword and provides automatic version resolution for all nodes.
What the registry contains¶
For each n8n node, the registry stores only what the compiler needs:
- version — the latest
typeVersionnumber - group — whether it's a trigger, transform, etc.
- inputs/outputs — connection types (main, ai_tool, ai_memory, etc.)
- credentials — which credential types the node accepts
- properties — every parameter with its name, type, default value, valid options, and nested children
UI-only fields (displayName, description, icon, placeholder, displayOptions, routing) are stripped to keep the registry compact.
How the compiler uses it¶
-
Version resolution — When emitting
typeVersion, the compiler checks hardcoded values first (for ergonomic keywords), then falls back to the registry. This means even aNODE "n8n-nodes-base.notion"call gets the correct version without any hardcoding. -
Type-aware serialization — The
NODEkeyword uses registry property types to serialize DSL values into the correct JSON format. For example, aresourceLocatorproperty is automatically wrapped as{"__rl": true, "mode": "url", "value": "..."}. -
Graceful fallback — If
node-registry.jsonis not present, the compiler still works for all ergonomic keywords. Only theNODEkeyword benefits from the registry; it is not required for existing syntax.
Regenerating the registry¶
The registry is extracted from a built n8n repository:
# 1. Build n8n (requires the n8n source repo)
cd /path/to/n8n
pnpm install
pnpm build --filter=n8n-nodes-base...
pnpm build --filter=@n8n/n8n-nodes-langchain...
# 2. Extract into nflow project
python3 scripts/extract-node-registry.py /path/to/n8n \
-o node-registry.json \
--include-credentials \
--stats
This reads dist/types/nodes.json from both packages/nodes-base (core nodes) and packages/@n8n/nodes-langchain (AI nodes), filters to compiler-relevant fields, keeps only the latest version of each node, and writes the registry.
Parameter type mapping¶
The registry tracks n8n property types. The compiler serializes each type as follows:
| n8n Property Type | JSON Output Format |
|---|---|
string |
Raw string, or "={{ expr }}" for expressions |
number |
Number |
boolean |
true / false |
options |
One of the allowed value strings |
multiOptions |
Array of value strings |
json |
Raw JSON string or object |
collection |
{ key: value, ... } object |
fixedCollection |
{ groupName: { ... } } or { groupName: [{ ... }] } |
resourceLocator |
{ __rl: true, mode: "url"/"id"/"list", value: "..." } |
filter |
n8n filter condition structure |
assignmentCollection |
{ assignments: [{ name, value, type }] } |