Docs
Reference

n8n Import (CLI Template)

Quick Answer

Use this guide to import n8n workflows into Breyta with runnable step mappings, fallback rules, and migration-safe patterns.

Purpose: import an n8n workflow JSON into a runnable Breyta flow ASAP with best-effort translations.
Keep all prompts, code, request bodies, and messages like-for-like in the output.

This reference is written for coding agents that generate the EDN flow file.

Import loop (plan-first)

  1. Parse n8n JSON and list nodes, connections, credentials, and expressions.
  2. Map triggers and nodes to Breyta equivalents (tables below).
  3. Mark unsupported/custom nodes and choose a runnable fallback.
  4. Generate :requires slots from credentials (strip keys/secrets).
  5. Emit :templates / :functions and the :flow orchestration.
  6. Configure-check and run (optionally validate for explicit read-only checks).

Do not review or critique the workflow design. The importer's job is translation, not improvement.
If something is missing or unclear, add a TODO(n8n-import) note instead of suggesting optimizations.
Do not add “conversion notes” that propose changes (e.g., persistence, schedules, model tweaks) unless asked.

Import loop (step-by-step, strict)

Use this when reliability matters more than speed

  1. Translate exactly one node into a Breyta step
  2. Run the step in isolation with a small input and inspect the output shape
  3. Fix shape and size issues before translating the next node
  4. Only then move on to the next node in the graph

This avoids mismatched data shapes and large payload failures later in the flow

Persist-first defaults

Assume many HTTP steps will exceed the inline 512 KB limit

  • Add :persist {:type :blob} to HTTP steps unless you can confirm the payload is small
  • :persist removes the inline :body. Downstream steps must read the blob or use :body-from-ref or :from-ref
  • If a downstream function expects inline JSON, add a read step that restores the same shape as the original HTTP response

Reliability notes from recent imports

  • Workspace-target config and installation-target config are distinct. Verify you configured the same target you are running.
  • Move URL query params into :query so they are not lost when you split base URL and path
  • Persisted HTTP responses do not include inline :body. Add a read step or use :body-from-ref
  • Align step output keys to downstream expectations. If a step outputs :html, but the next step expects :content, add an alias
  • Slow HTTP endpoints can time out even when they complete. Prefer :timeout on the HTTP step when a service is known to take longer
  • For long LLM calls, wrap them in flow/poll and set a :return-on clause that checks :content

Naming rules

  • Step id: n8n node name -> kebab-case keyword (no n8n- prefix).
  • Normalize: lower-case, then replace non [a-z0-9-] with -, collapse repeats, trim -.
  • If the name is empty or starts with a non-letter, prefix with step-.
  • De-dupe: append -2, -3, etc for collisions.
  • Template ids: :<step-id>-request, :<step-id>-prompt, :<step-id>-sql.
  • Function ids: :<step-id>-fn.
  • Preserve the original n8n node name in a ;; comment above the step.

Keep content like-for-like

  • Prompts/messages/HTTP bodies: copy verbatim into :templates (preferred) or :json/:body.
  • n8n expressions ({{$json...}}, {{$node...}}) do not run in Breyta.
    • Translate expressions into a :function step that computes the needed values.
    • Preserve the original expression in a comment block when translation is non-trivial.
  • The CLI importer translates a conservative subset automatically:
    • $json paths, including bracket paths such as {{$json["query"]["first_name"]}}.
    • Simple binary +, ternaries, and post-increment expressions.
    • $node["Name"].json... references only when the referenced node has already been bound. Forward references stay as TODOs.
    • HTTP URL/query templates with simple $json/safe $node paths become Breyta Handlebars variables in the flow-local HTTP template.
  • Prefer a per-node “prep” function that computes all expression-derived fields, then pass its output into the step/template.
  • Code nodes: attempt a real translation to a Breyta :function (Clojure) so the flow runs as intended.
    • Only fall back to (fn [input] input) when translation is unclear; keep the original code verbatim in comments.
  • Java interop in :function steps is restricted (small allowlist). Prefer breyta.sandbox helpers.
    For time/date, you can use allowlisted java.time.* for parsing/formatting, but avoid deriving "now" in code;
    pass timestamps/flags via inputs (e.g. :weekday) or use runtime-provided time utilities, and add a TODO if missing.

Example comment block:

;; TODO(n8n-import): port JS code below to Clojure
;; --- begin n8n code ---
;; <verbatim code here>
;; --- end n8n code ---

Example expression translation note:

;; TODO(n8n-import): translated n8n expression {{$json.user.id}} to (get-in input [:user :id])

Expression translation quick map (best-effort)

Use a :function step to compute these values:

  • {{$json}} -> input
  • {{$json.foo}} -> (get input :foo) / (get-in input [:foo])
  • {{$json.foo.bar}} -> (get-in input [:foo :bar])
  • {{$node[\"Node Name\"].json}} -> <node-binding>
  • {{$node[\"Node Name\"].json.foo}} -> (get-in <node-binding> [:foo])
  • {{$node[\"Node Name\"].json[\"page\"]++}} -> (inc (or (get-in <input-map> [:node-name :page]) 0)) when the node is available.
  • {{$json.foo + 1}} -> (+ (get input :foo) 1)
  • {{$json.foo ? \"yes\" : \"no\"}} -> ^{:label \"Condition\" :yes \"true\" :no \"false\"} (if (get input :foo) \"yes\" \"no\")
  • {{$json.a + $json.b}} -> (+ (get input :a) (get input :b))
  • {{$now}} -> (flow/now-ms) (confirm time semantics)
  • {{$today}} -> derive from (flow/now-ms) (TODO if date formatting is needed)

If an expression uses n8n-only helpers (e.g., $items(), $runIndex, $prevNode), add a TODO and compute it explicitly in a function.

Items vs maps (n8n items -> Breyta data)

n8n nodes operate on arrays of items; Breyta steps receive a map.
Import rule:

  • Wrap n8n items as {:items [...]} in Breyta.
  • Use :function steps to map/filter/reduce items explicitly (data transformation).
  • Default for side‑effects: iterate per item with for unless the n8n node is explicitly “first item only”.

Example:

{:items [{:id 1} {:id 2}]}
(flow/step :function :map-items
           {:ref :map-items-fn
            :input {:items items}})

Example (side‑effects per item):

(let [items (:items input)
      results ^{:label "Process each item"}
              (for [item items]
                (flow/step :http :post-item
                  {:connection :api
                   :path "/items"
                   :method :post
                   :json item}))]
  {:results (vec results)})

Common migration pitfalls (watch for these)

  • Query params: move URL query strings into :query so the runtime includes them
  • Auth placement: keep header vs query as in n8n, do not guess
  • Lazy seqs: wrap for output with vec before passing into :function steps
  • Java interop: restricted allowlist; prefer breyta.sandbox helpers; for time, use flow/now-ms + explicit inputs
  • Regex: do not use \\s, use explicit character classes
  • Persisted HTTP: inline :body is omitted, add a read step or :body-from-ref if downstream expects JSON

Split In Batches:

  • Use (partition-all n items) in a :function step, then for over batches.

Graph translation (multi-input / branching)

Convert the n8n graph into a topologically ordered let form:

  1. Build a DAG from connections.
  2. Order nodes so inputs are bound before use.
  3. For multi-input nodes, pass a map of upstream results.

Example multi-input:

(let [a (flow/step :http :fetch-a {...})
      b (flow/step :http :fetch-b {...})
      merged (flow/step :function :merge
                        {:ref :merge-fn
                         :input {:left a :right b}})]
  merged)

Branching:

^{:label "Branch decision"
  :yes "True branch"
  :no "False branch"}
(if condition
  (flow/step :http :true-branch {...})
  (flow/step :http :false-branch {...}))

Loops / Split in Batches:

  • Use for for orchestration over items/batches.
  • Use :function to build batches (partition-all) and to reduce results.
  • For cyclic n8n graphs, only reference nodes that have already been safely bound in the emitted let. Pass those references explicitly as an input map, for example {:set set :increment increment}.
  • Do not synthesize a forward reference to a later node. If the loop requires true repeated execution, keep a single-pass fallback and add a TODO.
  • If you cannot preserve the loop semantics safely, add a TODO and keep a single-pass fallback.

Trigger mapping

n8n triggerBreyta surfaceNotes
Manual Trigger:interfaces {:manual [...]}Always include one manual interface for run-now testing.
Webhook Trigger:interfaces {:webhook [...]}See GUIDE_WEBHOOKS_AND_SECRET_REFS.md. Requires auth + secret slot. Legacy webhook event triggers are accepted only for compatibility.
Cron / Schedule:schedulesMap cron/timezone if present; else TODO.
Interval / Polling:schedulesConvert to cron if possible; otherwise TODO.
Service triggers (Slack, GitHub, etc.):interfaces :webhook or :schedulesIf the service supports webhooks, default to webhook interface + TODO.

Minimal webhook interface + secret:

:requires [{:slot :webhook-secret
            :type :secret
            :secret-ref :webhook-secret
            :label "Webhook Secret"}]
:invocations {:default {:inputs []}}
:interfaces {:webhook [{:id :inbound-webhook
                        :invocation :default
                        :event-name "inbound.webhook"
                        :auth {:type :api-key
                               :secret-ref :webhook-secret}}]}

Step mapping (best-effort)

n8n nodeBreyta stepHow to translate
HTTP Request:httpPrefer :template with :request (URL -> base-url + path).
Webhook Response:functionReturn a response map: {:status 200 :headers {} :body ...}. See GUIDE_WEBHOOKS_AND_SECRET_REFS.md#webhook-response-maps.
Set:functionMap/merge fields into a new map.
Item Lists:functionFor split-out fields, normalize the selected field to :items and :<field>-items. Verify exact batching semantics.
HTML Extract:functionSimple id selectors (for example #firstHeading) can be extracted from :body/:html; complex selectors remain TODOs.
Code (JS/Python):functionTranslate to Clojure; only use placeholder + TODO when unclear.
IF / Switchflow if/cond + :functionIf expression maps cleanly, use it; else TODO and default to a safe false branch (or pass-through) to avoid unintended side effects.
Merge:function(merge a b) or custom merge logic.
Wait / Delay:waitUse :timeout or placeholder with TODO.
Database (Postgres/MySQL/etc.):dbPut SQL in :template and pass :params.
LLM / OpenAI / AI nodes:llmConvert prompt/messages to :template and map inputs.
NoOp / Start:functionIdentity function.

HTTP Request translation

If the n8n node has a full URL, split it:

  • :base-url = scheme + host
  • :path = path + query (or use :query map)
  • For expression URLs, translate simple n8n expressions to Handlebars template variables and pass the resolved node/input map as :data.
  • Example: =https://api.github.com/users/{{$node["Set"].json["githubUser"]}}/starred becomes path /users/{{set.githubuser}}/starred with :data {:set set}.
  • Query expressions follow the same rule: ={{$node["Set"].json["page"]}} becomes "{{set.page}}".

Example:

:requires [{:slot :api
            :type :http-api
            :label "Imported API"
            :base-url "https://api.example.com"
            :auth {:type :api-key}}]

:templates [{:id :fetch-user-request
             :type :http-request
             :request {:path "/users/{{id}}"
                       :method :get
                       :headers {"Accept" "application/json"}}}]

(flow/step :http :fetch-user
           {:connection :api
            :template :fetch-user-request
            :data {:id user-id}})

Credentials -> :requires (strip secrets)

n8n exports often include credential values. Do not copy secrets.

Rules:

  • For each unique credential reference, create a :requires slot.
  • Use :type based on node family:
    • HTTP nodes -> :http-api
    • DB nodes -> :database
    • LLM nodes -> LLM-capable :http-api connections
    • Unknown/custom -> :secret
  • Include :label derived from credential name or node name.
  • Leave auth type as best-guess (often :api-key), but never include keys.
  • If :base-url cannot be derived, set a placeholder and add TODO to fill.
  • Do not second‑guess auth placement (header vs query) unless the n8n node explicitly specifies it; copy the n8n config as-is and add a TODO if unclear.

Example:

:requires [{:slot :stripe
            :type :http-api
            :label "Stripe API"
            :base-url "https://api.stripe.com"
            :auth {:type :bearer}}]

Unsupported or custom nodes

Always emit a runnable fallback step and add a TODO:

  • Default fallback: choose based on intent.
    • If the node performs an API call or has a URL/credentials -> :http.
    • Otherwise -> :function.
  • Add ;; TODO(n8n-import): comment describing what to implement.
  • Add a web-search note: “Search the web for API docs to rebuild this node as HTTP.”

Example:

;; TODO(n8n-import): Custom node "Acme CRM" not supported.
;; TODO(n8n-import): Search the web for Acme CRM API docs and rebuild as HTTP.
(acme (flow/step :http :acme
                 {:url "https://api.acme.com"  ;; placeholder
                  :method :post
                  :json {}}))

Runnable ASAP defaults

  • Always include a manual interface, even if the n8n flow is only webhook/schedule.
  • If a step cannot be translated, emit a placeholder :function that returns input.
  • Keep the flow deterministic; avoid non-deterministic side effects in placeholder logic.
  • Only return response maps for webhook-interface flows; otherwise return normal flow output.
  • Do not add “review” commentary about missing steps or improvements; only translate and note TODOs.

CLI import workflow (Breyta‑style)

Follow the Breyta CLI loop so the imported flow is testable fast:

  1. Generate the first-pass EDN file with breyta flows import n8n workflow.json --slug <slug>
    • Default output: ./tmp/flows/<slug>.clj
    • Use --out ./tmp/flows/<slug>.clj to choose a path explicitly
    • Use --server-validate when an API target is configured and you want the importer to push the generated draft and run flows validate immediately.
  2. Review in-file TODO(n8n-import) notes and complete unsupported/custom semantics.
  3. If you did not use --server-validate, run breyta flows push --file ./tmp/flows/<slug>.clj (this updates draft target).
  4. Configure required slots/inputs (no secrets in repo), for example breyta flows configure <slug> --set <slot>.conn=conn-...
  5. breyta flows configure check <slug>
  6. Optional read-only verification after later edits: breyta flows validate <slug>
  7. breyta flows run <slug> --input '{}' --wait

Server validation is opt-in because it mutates the workspace draft. It requires --api/BREYTA_API_URL,
workspace, and auth flags/env to point at the intended workspace. A successful response includes
data.serverValidation.pushedDraft: true and data.serverValidation.valid: true.

If you build a converter script:

  • Prefer adding it to breyta-cli so agents can call it directly, or use a real file over large inline heredocs.
  • Run python -m py_compile before executing.
  • Avoid f-strings that contain backslashes inside {...}; precompute strings or use .format.

Notes:

  • flows push updates the draft working copy and draft-target bindings/config.
  • flows import n8n --server-validate also updates the draft working copy, then validates source=draft.
  • flows run still needs an active version by default, so brand-new flows must be deployed or released before that command will run them.
  • flows release is an advanced rollout step for live-install behavior.
  • A real public-template fixture currently validates server-side with only review TODOs left for sticky notes and unresolved IF condition semantics after supported data-transform and safe back-reference translations.

If breyta is not on PATH:

  • Ask the user for the correct CLI path or to add it to PATH, then re-run the same commands.

Regex conversion guardrails:

  • Do not inject literal newlines into regex literals (e.g., #"\r?\n" must keep \\n).
  • Prefer re-pattern with a normal string when in doubt: (re-pattern \"^...$\").
  • Avoid unsupported escapes (e.g., \\s). Use character classes instead: [\\t\\n\\r ] or \\p{Space} if supported.
  • Do not propose using \\s as a fix; it remains unsupported. Replace with explicit character classes.
  • If a push fails with a regex parse error, fix the pattern and re-run; do not speculate in output.
  • When reporting fixes, state only the concrete change (e.g., “replaced \s with [\t\n\r ]”), not guesses.
  • Do not switch algorithms (e.g., replace regex with substring logic) unless the user asks; add a TODO instead.
  • Avoid double-escaping braces/backslashes in regex literals; prefer #\"\\{.*\\}\" (single escaping) or re-pattern with a normal string.
  • Do not run blanket regex-escape rewrites across the flow. Fix only the specific pattern that failed and only if you can describe the exact change.
  • Do not invent root-cause speculation (e.g., “metadata parsing issues”) without evidence. If parsing fails, report the error and inspect the exact offending literal.
  • Do not run unrelated shell experiments (e.g., clojure -e probes) unless the user explicitly asks.

Output expectations

  • Output only the translated flow file path and a short TODO list derived from in‑file TODO(n8n-import) notes.
  • Default behavior: after import, push the flow, configure it, and run flows configure check unless the user explicitly opts out.
  • If the user is new or asks for next steps, suggest the canonical workflow only (push → configure → configure check → run). Treat release/promote/installations as advanced.

Validation checklist

Use the CLI import workflow above; if you need a quick list, it’s the same steps.

  • Local importer validation checks delimiter balance, EDN readability, required top-level keys, quoted :flow, and supported invocation input types.
  • Server validation (--server-validate or flows push followed by flows validate) checks the generated source against the real Breyta flow compiler/validator.
  • Server validation does not prove every n8n semantic is identical. Remaining TODOs still need human/agent review before release, especially loops, custom nodes, complex selectors, and condition semantics that were defaulted safely.

Minimal example (n8n HTTP + Code)

n8n:

Breyta (sketch):

{:slug :imported
 :name "Imported Flow"
 :concurrency {:type :singleton :on-new-version :supersede}
 :requires [{:slot :api
             :type :http-api
             :label "Imported API"
             :base-url "https://api.example.com"
             :auth {:type :api-key}}]
 :templates [{:id :get-users-request
              :type :http-request
              :request {:path "/users"
                        :method :get}}]
 :functions [{:id :transform-users-fn
              :language :clojure
              :code "(fn [input] input)"}]
 :invocations {:default {:inputs []}}
 :interfaces {:manual [{:id :run :label "Run" :invocation :default :enabled true}]}
 :flow
 '(let [input (flow/input)
        request ^{:label "Request source"
                  :yes "Use input override"
                  :no "Use imported template"}
                 (if (:request input) (:request input) {})
        users (flow/step :http :get-users
                         {:connection :api
                          :template :get-users-request
                          :data request})
        ;; TODO(n8n-import): port JS code below to Clojure
        ;; --- begin n8n code ---
        ;; <verbatim n8n code>
        ;; --- end n8n code ---
        result (flow/step :function :transform-users
                          {:ref :transform-users-fn
                           :input {:users users
                                   :input input}})]
    result)}

Related

As of May 20, 2026