Flow Definition
Canonical Breyta flow definition reference for required fields and high-value semantics.
Quick Answer
A Breyta flow is an EDN map with identity, concurrency, triggers, deterministic :flow, and optional requirements/templates/functions.
Fields At A Glance
| Field | Required | Purpose | Notes |
|---|---|---|---|
:slug | Yes | Stable flow identifier | Used by CLI/API commands. |
:name | Yes | Human-readable display name | Safe to change without changing slug identity. |
:concurrency | Yes | Overlap/version execution policy | Defines overlap rules and version behavior. |
:triggers | Yes | Run entry points | Supports manual (:manual), schedule (:schedule + cron), and webhook/event (:event + :config {:source :webhook ...}). |
:flow | Yes | Deterministic orchestration body | Keep orchestration-focused; move shaping/transforms to top-level :functions. |
:description | No | Additional context for operators/users | Optional metadata only. |
:deploy-key-required | No | Protect release/promotion operations behind a shared deploy key | Stored as flow metadata; when true, guarded release/promotion operations require a valid key. |
:requires | No | Runtime dependencies and setup/run form inputs | Declares connection/secret/form requirements resolved via bindings/profile inputs. Use :provided-by :author for requirements satisfied by the flow author instead of the installation user. For :kind :form, use :collect :setup (default) for setup entered once and reused on later runs, or :collect :run for input required on each run. |
:templates | No | Reusable large static content | Best for request bodies, prompts, queries, notifications. |
:functions | No | Reusable Clojure transforms | Call via flow/step :function refs from :flow. |
:tags | No | Classification/feature tags | Optional metadata only; does not gate install/run behavior. |
Manual Trigger Labels
For manual triggers, :label is also reused as the manual run CTA across Resource UI surfaces.
That includes:
- the flow page run action
- installation and launch page primary run buttons
- the setup side panel run button
If multiple manual triggers are declared, Resource UI currently uses the first manual trigger label.
If no manual trigger label is set, the fallback copy is Run now.
Example:
{:triggers [{:type :manual
:label "Ask project question"
:enabled true
:config {:fields [{:name :question
:type :text
:label "Question"
:required true}]}}]}
Deploy-Key Guard
Use :deploy-key-required to gate release operations for a flow:
{:slug :critical-flow
:name "Critical Flow"
:deploy-key-required true
:concurrency {:type :singleton :on-new-version :supersede}
:flow '(let [input (flow/input)] input)}
Behavior:
:deploy-key-required truerequires a valid deploy key for guarded release operations (for exampleflows releaseandflows promote).- Disabling the guard (
:deploy-key-required false) also requires a valid deploy key. - Enabling the guard fails unless
BREYTA_FLOW_DEPLOY_KEYis configured for the Breyta environment. - The deploy key is not modeled as
:requires, bindings, or a connection. It is a server-side release secret.
Draft Vs Live (Definition Semantics)
The flow definition fields are the same, but runtime resolves them from different targets:
| Aspect | draft target | live target |
|---|---|---|
| Definition source | Latest pushed working copy in workspace. | Installed released version. |
| How target changes | breyta flows push --file ... | breyta flows release <slug> or breyta flows promote <slug> |
:requires resolution | breyta flows configure <slug> ... values. | breyta flows configure <slug> --target live ... or installation-specific configuration values. |
| Webhook trigger endpoint surface | Development/staging-oriented trigger endpoints for workspace iteration. | Installed target endpoints for consumer/runtime traffic; can differ from draft endpoints. |
| Typical verification run | breyta flows run <slug> --wait | breyta flows run <slug> --target live --wait |
| Best use | Authoring iteration and fast feedback. | Stable consumer/runtime behavior. |
flows push does not modify live; it only updates draft.
Form Requirements
Use {:kind :form ...} inside :requires when the flow needs structured user input.
Top-level form requirement keys:
| Key | Required | Meaning |
|---|---|---|
:kind | Yes | Must be :form. |
:fields | Yes | One field map or a vector of field maps. |
:label | No | Group label shown in setup/run UI. |
:collect | No | :setup (default) means fill once and reuse; :run means ask every run. |
:provided-by | No | :installer (default) or :author. Author-provided requirements are not shown to the installer. |
Field keys:
| Key | Required | Meaning |
|---|---|---|
:key or :name | Yes | Stable submitted field id. |
:label | No | Display label in UI. |
:field-type | No | Input type. Defaults to :text. |
:required | No | When true, the field must be filled before save/run. |
:placeholder | No | Helper text shown in the input UI. |
:default | No | Default value shown when the form first loads. |
:options | For :select | Allowed values. |
:multiple | For :resource | Allow selecting more than one resource. |
:resource-types | For :resource | Optional resource type filter such as [:file :result]. |
:accept | For :resource | Optional MIME filter such as ["application/pdf" "text/plain" "text/*"]. |
:slot | For :resource | Optional local blob-storage slot such as :archive. |
:storage-backend | For :resource | Optional explicit storage backend filter such as :gcs. Usually inferred from :slot when present. |
:storage-root | For :resource | Optional explicit storage root such as "reports/acme". Usually inferred from :slot when present. |
:path-prefix | For :resource | Optional relative path prefix under the effective storage root, such as "exports/2026". |
:source | For :resource | Legacy picker routing override. Omit in normal flow authoring. |
Supported :field-type values:
:field-type | UI behavior | Notes |
|---|---|---|
:text | Single-line text input | Default when omitted. |
:textarea | Multi-line text box | Good for prompts and instructions. |
:select | Dropdown | Requires :options. |
:boolean | Checkbox/toggle | Required means the value must be true. |
:number | Numeric input | Submitted as text, validated/coerced by the flow contract. |
:date | Date picker | Browser-native date input. |
:time | Time picker | Browser-native time input. |
:datetime | Date-time picker | Browser-native datetime-local input. |
:resource | Search/select workspace resources | Run-only for now; use with :collect :run. |
Resource field expectations:
:field-type :resourceis currently supported only with:collect :run.- The run UI renders a resource picker only when a resource field is declared.
- Selected values are passed as resource references, not full file contents.
- Use
:multiple truewhen the flow expects a collection of resources. :acceptmatches resource MIME types. Use exact values like"application/pdf"or wildcard prefixes like"text/*".:resource-typesand:acceptare combined, so a resource must satisfy both filters when both are present.- Resources without matching MIME metadata are excluded when
:acceptis set. - Use
:slot <slot>to scope the picker to a declared local blob-storage slot. - When
:slotpoints at an installer-owned blob-storage slot, the picker reuses that slot's configured storage root automatically. - Use
:path-prefixwhen the picker should narrow to a subfolder under the effective storage root. - Picker storage filters use normalized index fields:
storage_backend,storage_root, andpath_under_root. :storage-backendand:storage-rootare optional explicit filters for unbound resource pickers. Slot-bound pickers infer them from the resolved slot binding.- Installer-owned
:blob-storageslots automatically add required setup controls for the connection binding and storage root; picker scoping reuses that configured root automatically. - For end-user installations, the effective default root is private by default:
installations/<profile-id>/<prefix>, derived from the authored prefix until an explicit root is saved. - For platform-backed persists, that configured root becomes the exact write base:
workspaces/<ws>/storage/<root>/...with no extra hidden flow or step segments. :sourceis still accepted as a legacy/internal override, but normal authored flows should not need it.
Practical installer model:
- local installer-owned slot such as
:archive: the author declares a semantic prefix such as"reports", and an end-user installation derives a private effective root such asinstallations/<profile-id>/reportsuntil an explicit root is saved - another flow shares only when its own local slot is explicitly configured to the same concrete storage location
:preferson a blob-storage slot records intended upstream sharing, but it does not auto-select or persist the consumer root- top-level
:connectionsremains display metadata for icons and labels; blob-storage authoring lives in:requires
That means the authoring surface stays local:
{:requires [{:slot :archive
:type :blob-storage
:label "Archive storage"
:config {:prefix {:default "reports"}}}]}
and the installation resolves that local name concretely:
An end-user installation with profile prof-1 therefore starts with an effective root of:
installations/prof-1/reports
If the installer later chooses an explicit shared root, that saved binding wins:
{:bindings {:archive {:binding-type :connection
:connection-id "platform"
:config {:root "reports/customer-a"}}}}
Optional setup hint for cross-flow sharing:
{:requires [{:slot :archive
:type :blob-storage
:label "Archive storage"
:prefers [{:flow :daily-report-export
:slot :archive}]
:config {:prefix {:default "reports"}}}]}
:prefers does not rewrite the scoped default root. To share, the installer still must explicitly save the same connection + root on both installations.
How form fields appear to the flow:
- Setup-collected fields and run-collected fields both appear in
flow/input. - The flow reads them by their declared
:key/:name. - Resource fields arrive as one resource ref or a vector of resource refs, depending on
:multiple.
Examples:
{:requires [{:kind :form
:collect :setup
:label "Setup"
:fields [{:key :region
:label "Region"
:field-type :select
:required true
:options ["EU" "US"]}]}
{:slot :archive
:type :blob-storage
:label "Archive storage"
:config {:prefix {:default "reports"
:label "Folder prefix"
:placeholder "reports/customer-a"}}}]}
{:connections [{:icon-url "/assets/connections/storage.svg"
:label "Storage"}]}
{:requires [{:kind :form
:collect :run
:label "Run input"
:fields [{:key :question
:label "Question"
:field-type :text
:required true}
{:key :resources
:label "Resources"
:field-type :resource
:slot :archive
:required true
:multiple true
:accept ["application/pdf" "text/plain"]}]}]}
{:requires [{:slot :archive
:type :blob-storage
:label "Archive storage"
:config {:prefix {:default "reports"}}}
{:kind :form
:collect :run
:label "Cross-flow run input"
:fields [{:key :report
:label "Archived report"
:field-type :resource
:required true
:accept ["application/pdf"]
:slot :archive}]}]}
{:requires [{:kind :form
:collect :run
:label "Run input"
:fields [{:key :resources
:label "Text resources"
:field-type :resource
:required true
:multiple true
:accept ["text/*"
"application/json"
"application/xml"
"application/edn"]}]}]}
{:flow
'(let [input (flow/input)]
{:region (:region input)
:question (:question input)
:resources (:resources input)})}
Example runtime input:
{:region "EU"
:question "What changed?"
:resources [{:type :resource-ref
:uri "res://v1/ws/ws-1/file/demo-a"}
{:type :resource-ref
:uri "res://v1/ws/ws-1/result/demo-b"}]}
Notes:
:resource-typesis optional. Omit it for the default file picker.- Persisted blob artifacts and uploads are
:fileresources. - Use
:resource-types [:result]only when the flow should browse structured run outputs or captured results instead of files. - For slot-bound file pickers, use
:path-prefixto narrow inside the installer-chosen root without hardcoding that root in flow source.
Supported Orchestration Constructs
Inside :flow, supported non-step forms are:
| Category | Constructs | Typical Use |
|---|---|---|
| Binding / sequencing | let, do | Build deterministic orchestration pipelines. |
| Branching | if, if-not, when, when-not, cond, case | Route execution by state or decision flags. |
| Iteration | for, doseq, loop, recur | Deterministic fan/loop orchestration. |
| Child flow orchestration | flow/call-flow | Delegate larger branches to child flows. |
| Polling helper | flow/poll | Bounded polling built on deterministic loop/sleep semantics. |
Use metadata labels to make branches and loops readable in run timelines:
^"Review Gate"shorthand label^{:label "Review Gate"}explicit label^{:label "Review Gate" :yes "Approved" :no "Rejected"}forif/if-not^{:label "Retry Loop" :max-iterations 5}forloop
Common Orchestration Shape
Use one orchestration layer in :flow, then delegate:
- shaping/normalization to
:functions - large static bodies/prompts/queries to
:templates - heavy outputs to
:persist - larger branches to child flows via
flow/call-flow
{:functions [{:id :prepare
:language :clojure
:code "(fn [input] {:order-id (:order-id input) :lookup-key (str \"orders:\" (:order-id input))})"}]
:flow
'(let [input (flow/input)
prepared (flow/step :function :prepare
{:ref :prepare :input input})
order (flow/step :http :fetch-order
{:connection :orders-api
:method :get
:path (str "/orders/" (:order-id prepared))
:persist {:type :blob}})
route (flow/call-flow :order-routing
{:order-ref (:ref order)
:lookup-key (:lookup-key prepared)})
result ^{:label "Apply routed action?"
:yes "Call apply flow"
:no "Skip apply"}
(if (:should-apply route)
(flow/call-flow :order-apply route)
{:status :skipped})]
{:order-ref (:ref order)
:route route
:result result})}
:persist Path Modes
Use the two blob persist modes like this:
:persist {:type :blob ...}without:slotwrites under the runtime-managed baseworkspaces/<ws>/persist/<flow>/<step>/<uuid>/...:persist {:type :blob :slot :archive ...}writes under the installer-bound storage baseworkspaces/<ws>/storage/<root>/...- In both modes,
:persist :pathstays relative to that base and:filenamestays the leaf name - Use
:slotonly when installers should control the storage root or when runtime resource pickers should reuse the same storage scope
Example:
{:flow
'(let [report-a (flow/step :http :download-inline-managed
{:connection :reports-api
:response-as :bytes
:persist {:type :blob
:path "exports/{{input.customer-id}}"
:filename "summary.pdf"}})
report-b (flow/step :http :download-slot-managed
{:connection :reports-api
:response-as :bytes
:persist {:type :blob
:slot :archive
:path "exports/{{input.customer-id}}"
:filename "summary.pdf"}})]
{:runtime-managed (:ref report-a)
:slot-managed (:ref report-b)})}
With input.customer-id = "cust-77":
- runtime-managed path:
workspaces/<ws>/persist/<flow>/<step>/<uuid>/exports/cust-77/summary.pdf - slot-managed path with root
reports/acme:workspaces/<ws>/storage/reports/acme/exports/cust-77/summary.pdf
Determinism Rules
- keep orchestration deterministic in
:flow - put side effects in
flow/stepboundaries - avoid non-deterministic random/time calls in workflow body logic
Limit-Aware Authoring
- payload limit is 150 KB for the core flow definition map (excluding template/function content)
- effective total package can be ~2.1 MB with template/function totals
- step output and webhook payload limits are enforced at runtime
- use templates for large static content and treat
:persistas a common default for variable-size step outputs
See limits: Limits And Recovery