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, setup requirements,
per-run invocation inputs, callable interfaces, optional schedules, reusable
definition surfaces (:templates, :functions, packaged :steps, reusable
:agents), and finally deterministic :flow orchestration.
The source format is a Breyta DSL with Clojure/EDN syntax and binding logic, not
a normal Clojure program. Use Clojure syntax to declare data, functions, and
flow/step calls, but keep the mental model as orchestration: side effects
belong at step boundaries, workflow body logic should stay deterministic, data
must remain serializable, and large/variable outputs should be persisted as
resource refs instead of carried inline.
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. |
:flow | Yes | Deterministic orchestration body | Keep orchestration-focused; move shaping/transforms to top-level :functions. |
:description | No | Operator/user context | Sanitized Markdown on setup/run pages. |
: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 | Setup dependencies | Connections/secrets/setup/worker requirements. Per-run fields belong in :invocations. |
:invocations | No | Per-run input contracts | Inputs supplied by users, webhooks, CLI, or clients. Use :default for normal flows. |
:interfaces | No | Callable ingress surfaces | Manual, HTTP, webhook, or MCP surfaces over invocation contracts. |
:schedules | No | Time-based automation | Declares cron/preset schedules that call an invocation. New scheduled automation belongs here. |
: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. |
:steps | No | Flow-local packaged steps | Qualified ids only, for example :github/open-pr. |
:agents | No | Flow-local named agent definitions | Qualified ids only, for example :review/security. Reusable agent configurations invocable from flow/step and publishable as tools for other agents. |
:providers | No | Flow-local provider definitions | Register custom LLM and files providers under {:llm [...] :files [...]}. Referenced via :provider in :llm, :agent, and :files steps. |
:mcp | No | Flow-level MCP tool adapters | Reuses :http-api connection slots with :backend :mcp; adapters allowlist server tools and publish selected tools via :tools {:mcp [...]}. |
:mcp-registries | No | Optional MCP registry metadata | Authoring metadata for known remote MCP endpoints; runtime exposure still requires explicit entries in :mcp and step-local :tools. |
:tags | No | Classification/feature tags | Optional metadata only; does not gate install/run behavior. |
Final Output Contract
The flow definition does not declare a separate output schema today. The
implicit contract is:
final value returned by :flow = run output
That final value is captured as the canonical flow-output resource and is also
normalized into the user-facing Output page artifact. Treat the last expression
of :flow as part of the flow's product design, not only as debug data.
Invocation Adapters
:invocations is the callable contract. The normal authoring model is one
solution flow with one public :default invocation. Runtime surfaces adapt into
that contract:
- manual interfaces provide a human run button/form
- schedules provide time-based activation
- webhook interfaces provide event intake, auth, replay/idempotency, and
generated endpoint delivery - HTTP and MCP interfaces expose the same contract to external clients and tools
For new flows, put per-run payload fields in :invocations {:default ...} and
reference them from an interface or schedule with :invocation :default. Use
additional invocation ids only when a manual form, webhook payload, schedule
payload, or private/internal caller needs a genuinely different input contract.
Do not use webhook event config as a generic HTTP API shape, and do not put new
manual or schedule entrypoints under the older entrypoint map. New source belongs
under :interfaces and :schedules; old trigger definitions are compatibility
source only.
:interfaces exposes the solution invocation to humans and external systems.
Use it when a manual form, another program, coding agent, integration, or
webhook provider should call the flow through an explicit surface.
{:invocations {:default
{:inputs [{:name :domain
:type :text
:label "Domain"
:required true}]}}
:interfaces {:http [{:id :enrich-company
:invocation :default
:enabled true
:method :post
:path "/companies/enrich"
:auth :workspace-api-auth}]
:webhook [{:id :company-updated
:invocation :default
:enabled true
:event-name "companies.updated"
:auth {:type :hmac-sha256
:header "X-Signature"
:secret-ref :company-webhook-secret}}]
:mcp [{:tool-name "enrich_company"
:description "Research and score a company from a domain."
:invocation :default
:enabled true
:auth :workspace-api-auth}]
:manual [{:id :run
:label "Run enrichment"
:invocation :default
:enabled true}]}
:schedules [{:id :daily-enrichment
:label "Daily enrichment"
:invocation :default
:cron "0 8 * * *"
:timezone "UTC"}]}
Interfaces are explicit allowlists, not automatic publication of every
invocation. A flow should expose one public callable contract for the solution,
with at most one manual adapter, one HTTP adapter, one webhook adapter, and one
MCP adapter. If you are tempted to expose several unrelated tools from one flow,
split the solution into separate flows instead. Runtime access is still gated
by the selected installation/live target, setup/bindings, interface enabled
state, paid entitlement where applicable, and current workspace/API
authentication. When another flow, coding agent, or external client reuses a
public flow, the installed HTTP or MCP interface is the preferred composition
surface because it applies the installer's setup, auth, billing entitlement, and
ownership. MCP clients should connect with a scoped workspace API credential for
the installation/interface surface; tool discovery should not expose
non-interfaced or unauthorized invocations.
Keep auth on the interface adapter, not on the invocation. The same invocation
can be called by manual UI, CLI, HTTP, MCP, schedule, or webhook adapters; each
adapter owns its own caller authentication.
Webhook Interfaces
Webhook ingress is authored as an interface over an invocation contract:
{:requires [{:slot :webhook-signing-secret
:type :secret
:label "Webhook Signing Secret"}]
:invocations {:default
{:inputs [{:name :order-id
:type :text
:label "Order ID"
:required true}]}}
:interfaces {:webhook [{:id :orders-updated
:invocation :default
:enabled true
:event-name "orders.updated"
:auth {:type :hmac-sha256
:header "X-Signature"
:secret-ref :webhook-signing-secret}}]}}
event-name controls the generated webhook event name. If omitted, Breyta uses
the interface :id. During installation/runtime sync, Breyta materializes the
webhook interface as the installation-owned webhook runtime endpoint and renders the
generated endpoint in setup/CLI surfaces.
Legacy webhook definitions are still read and materialized so existing flows
continue to run, but new source should use :interfaces {:webhook [...]}.
Author/consumer CLI loop:
breyta flows push --file ./company-intel.clj
breyta flows run company-intel --input '{"domain":"example.com"}' --wait
breyta flows interfaces call company-intel enrich-company --input '{"domain":"example.com"}' --wait
breyta flows release company-intel
breyta flows interfaces list company-intel --target live
breyta flows interfaces show company-intel enrich-company --target live
breyta flows interfaces curl company-intel enrich-company --target live --input '{"domain":"example.com"}'
breyta flows interfaces call company-intel enrich-company --target live --input '{"domain":"example.com"}' --wait
breyta flows metrics company-intel --source live
For a specific end-user installation:
breyta flows installations interfaces <installation-id>
breyta flows interfaces call company-intel enrich-company --installation-id <installation-id> --input '{"domain":"example.com"}' --wait
breyta flows metrics company-intel --installation-id <installation-id>
Author-owned interface endpoints are flow-source scoped. Draft and live are
separate surfaces, so generated endpoints include draft or live in the path:
/api/flows/{flow-slug}/interfaces/draft/{interface-id}
/api/flows/{flow-slug}/interfaces/live/{interface-id}
Workspace-scoped forms remain available as alternate compatibility URLs:
/api/workspaces/{workspace-id}/flows/{flow-slug}/interfaces/draft/{interface-id}
/api/workspaces/{workspace-id}/flows/{flow-slug}/interfaces/live/{interface-id}
/api/workspaces/{workspace-id}/flows/{flow-slug}/installations/{installation-id}/interfaces/{interface-id}
Installed consumer endpoints are installation scoped:
/api/flows/{flow-slug}/installations/{installation-id}/interfaces/{interface-id}
HTTP interface calls return the normal run envelope. When a run is accepted and a
workflowId is available, the response also includes data.statusUrl, scoped
to the same interface endpoint. Use that URL to poll long-running interfaces
without falling back to a broad run lookup.
MCP interface tools/call requests are optimized for coding-agent and LLM-agent
use. The server starts the backing flow run, waits for a terminal state up to a
bounded 180-second timeout, and returns the completed flow result in the MCP
response content and structuredContent. Breyta run metadata such as
workflowId, status, and status URL is attached under _breyta. Failed or
timed-out runs return a JSON-RPC tool error with the same scoped run metadata.
For installed public-flow reuse, point the MCP client at the installation-scoped
Streamable HTTP endpoint, authenticate with bearer auth, call initialize, then
tools/list, then tools/call with the advertised tool name.
Invocation metrics are aggregate entrypoint health signals for authors. They are
redacted by design and must not include raw inputs, outputs, headers, tokens,
secrets, or binding values. Metrics include interfaceScope, so author-owned
draft/live calls stay separate from installed consumer calls to the same
interface id.
Recommended output shapes:
- readable report: return
{:breyta.viewer/kind :markdown ...} - report with real resources: use Markdown plus fenced
breyta-resourceblocks - durable grid: persist a table and return or embed it as a table viewer
- media: persist a blob and return or embed an image/audio/video viewer
- structured data as the product: return a
:rawviewer
Do not expose internal res:// refs, raw debug maps, trace ids, or automation
payloads in public/end-user outputs unless that structured payload is explicitly
the product output. See Output Artifacts
for the full viewer and Markdown resource embed contract.
Recommended authoring order
Default pattern:
:requires
- declare connections, secrets, setup form inputs, and worker/runtime dependencies
- decide what the flow needs before writing behavior
- inventory and test existing workspace connections before inventing new setup
- choose stable slot names around business capability (
:github-api,:crm,:llm,:slack) rather than transient provider names - prefer reusing known-good connections and then shaping packaged
:steps/:agentsaround those slots - if a packaged tool wraps a broad connection-backed surface, add flow-local permission hints here before publishing it to an agent
:invocations
- declare per-run fields that a user, CLI command, interface, schedule, or client provides
- use
:defaultfor a single run form - point manual interfaces at the contract with
:invocation :default - put webhook ingress under
:interfaces :webhookand point it at the same
contract with:invocation :default
:interfaces
- declare manual, HTTP, webhook, and MCP call surfaces over invocations
- use
:manualfor Resource UI launch forms and "run now" actions - use
:webhookfor inbound provider events with explicit auth - use
:httpand:mcpfor programmatic clients and coding agents
:schedules
- declare cron/preset automation over invocations
- use stable schedule ids because setup overrides and CLI toggles address
schedules by id
:templates
- move prompts, request bodies, SQL, notification content, and other large
static text here
:functions
- put shaping, normalization, preparation, and projection logic here
:steps
- package heavy built-in step configs behind a narrower input/output contract
- especially for broad surfaces like
:http,:db,:notify,:function,
and:agent
:agents
- define reusable named agent roles here when the behavior is agent-shaped
- capture objective, instructions, memory, cost/evaluate/trace, and delegation
config once
:mcpwhen using remote MCP tools
- bind the remote endpoint as a normal
:http-apirequirement with
:backend :mcp - define a top-level MCP adapter with
:connection, optional:path,
:transport :streamable-http, and explicit:allow-tools - select adapter ids or individual tool refs from
:llm/:agentsteps via
:tools {:mcp [...]}
:flow
- wire those surfaces together in deterministic orchestration
- keep orchestration focused on sequencing, branching, waits, persistence, and
summaries
This is the preferred pattern for new flows. If you find yourself writing large
inline prompts, repeated transforms, or raw heavy built-in step configs directly
inside :flow, move that work up into one of the reusable top-level surfaces.
For agentic reviewer/fixer/coordinator flows, the default pattern should be:
:filesfor code/resource state- packaged
:stepsfor heavy external or policy-shaped operations - named
:agentsfor reviewer/fixer/coordinator roles - orchestration last
For installable flows, the authoring order matters more because it determines
the author/installer boundary:
:requiresdefines what the installer configures. Use:provided-by :author
for connections you want to absorb (LLM keys, internal APIs). Use installer-provided
for connections the installer owns (their GitHub repo, their database).:stepsand:agentsreference connection slots, not IDs. The slot name
resolves through:requiresbindings at runtime, so it works in both the
author's workspace and any installation.:memory,:cost,:trace,:evaluateare internal to the
flow — the installer never sees these resources.:output-persistwrites to the installer's workspace — this is the
product output they access.
See the Installable Agent Flows section
for a complete checklist and example.
MCP Tool Adapters
Remote MCP servers are modeled as HTTP API connections. Do not declare a
separate MCP connection type; use :type :http-api and :backend :mcp so
connection auth, base URL validation, SSRF protection, and installation binding
stay on the existing connection path.
Example:
{:requires [{:slot :linear-mcp
:type :http-api
:label "Linear MCP"
:backend :mcp
:base-url "https://mcp.linear.app"
:auth {:type :bearer}
:provided-by :installer}]
:mcp [{:id :linear/issues
:connection :linear-mcp
:transport :streamable-http
:allow-tools ["list_issues" "get_issue"]
:deny-tools ["delete_issue"]
:tool-prefix "linear"
:timeout-ms 15000
:max-result-bytes 200000
:tools [{:name "list_issues"
:description "List Linear issues."
:input-schema {:type "object"
:properties {:team_id {:type "string"}}
:required ["team_id"]}}]}]
:flow '(flow/step :agent :triage
{:connection :ai
:objective "Find the most relevant issue."
:tools {:mode :execute
:mcp [:linear/issues]}})}
Only explicitly selected tools are published to the model. Selecting an adapter
such as :linear/issues requires :allow-tools and publishes only those
allowed tools; selecting :linear/list_issues publishes just that named tool.
MCP calls run through the same agentic tool loop as built-in, packaged-step, and
agent-to-agent tools.
Connection slots are required unless marked optional. Use :optional true for
new definitions; :required false is accepted as the inverse setup alias for
connection slots.
Manual Interfaces
For manual launch, declare one :interfaces :manual entry that points at an
invocation. Its :label is 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 no manual interface label is set, the fallback copy is Run now.
Older manual entrypoint definitions remain accepted for existing flows, but new
source should use a manual interface.
Manual interface input fields are declared on the referenced invocation, not on
the manual interface entry itself. For author-run forms, put field maps under
:invocations {:default {:inputs [...]}}, then point
:interfaces {:manual [...]} at that invocation. Manual forms support scalar
fields, direct file upload fields, and resource-picker fields. See
Invocation Inputs for the canonical key list, supported
:type values, and upload examples.
Example:
{:invocations {:default
{:label "Ask project question"
:inputs [{:name :question
:type :text
:label "Question"
:required true}]}}
:interfaces {:manual [{:id :ask-project-question
:label "Ask project question"
:invocation :default
:enabled true}]}}
Schedules
Scheduled automation is authored as top-level :schedules entries that point
at invocations. Public installable schedule setup is available for every
schedule; no separate entrypoint source is required.
The authored schedule is the default schedule. Author setup overrides, if
saved, become the inherited default for installers. Each installer can then save
their own installation-scoped schedule settings and independently turn the schedule
on or off.
{:schedules [{:id :weekly-review
:label "Scheduled security review"
:invocation :default
:enabled false
:cron "0 9 * * MON"
:timezone "UTC"}]}
Current behavior:
| Field | Behavior |
|---|---|
| Schedules | Render schedule controls inside setup. |
| Saved choices | Stored as installation schedule settings. New installed schedules start disabled until enabled. |
| Author setup | Author overrides use the same controls. Reset restores authored defaults. |
| Time/day overrides | Daily/weekly/monthly cadences expose bounded time/day controls. Monthly last uses a guarded 28-31 cron window. |
| Timezone control | Saved/default zones win; detected zones fill gaps. |
| Source schedule config | Setup does not rewrite authored :cron or :timezone; schedule sync/runtime consumes saved schedule settings for installer-specific schedules. |
| Automatic execution state | Installed flows use per-schedule and per-interface enablement. |
| Webhook interface setup | Setup exposes webhook interfaces as individual switches. |
Schedule settings can also be changed through the CLI. Authors use
breyta flows configure <flow-slug> --schedules '{...}'; installers use
breyta flows installations configure <installation-id> --schedules '{...}'.
Use the authored schedule id as the schedule key. --schedule-enable <id> and
--schedule-disable <id> toggle one schedule without writing the full JSON
shape. --schedule-reset <id> removes a saved schedule override so the schedule
inherits the authored flow default again.
For a paid public flow, keep a manual run path available while validating the
schedule runtime path for installer-specific timezone, pause/resume, and failure
visibility.
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 interface endpoint surface | Flow-source scoped endpoints with draft in the path. | Flow-source scoped author testing endpoints with live in the path, plus installation-scoped consumer 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.
Connection-Scoped Packaged Tool Permissions
Connection requirements can carry flow-local runtime permission hints for
packaged tools. This is mainly useful when a packaged :steps definition wraps
:http and should only be allowed to call a narrow part of a broader API
connection.
These permissions live on the requirement slot, not on the workspace connection
record:
{:requires [{:slot :github-api
:type :http-api
:label "GitHub API"
:auth {:type :none}
:base-url "https://api.github.com"
:config {:tool-permissions
{:http {:mode :read-only
:allowed-hosts ["api.github.com"]
:allowed-path-prefixes ["/repos/breyta/breyta"]}}}}]}
The current generic HTTP permission hints are:
:mode:allowed-methods:allowed-hosts:allowed-path-prefixes:allowed-url-prefixes
Runtime enforces these guards when the packaged step executes, whether it is
invoked directly from flow/step or called as an agent tool through
:tools {:steps [...]}.
Worker Requirements
Use {:kind :worker ...} inside :requires when install behavior depends on external workers instead of installer-entered setup values.
Examples:
{:requires [{:kind :worker
:label "Security review workers"
:provided-by :author
:capability "codex-agent"
:job-types ["codex-security-review" "codex-security-fix"]
:provider "breyta/jobs"
:version "v1"}]}
{:requires [{:kind :worker
:label "Customer sync worker"
:provided-by :installer
:capability "customer-sync"
:job-types ["customer-sync"]
:provider "acme/customer-sync-worker"
:setup-summary "Deploy the packaged worker before enabling this installation."
:setup-docs-url "https://docs.example.com/customer-sync-worker"}]}
Modeling rules:
- Use
:requiresfor both setup inputs and external worker/runtime dependencies. - Use
:provided-by :authorwhen the flow author supplies the worker/runtime. - Use
:provided-by :installerwhen the installer or operator must run the worker/runtime. - A flow can declare more than one worker requirement when different job types or worker capabilities are needed.
- Prefer
:job-typesin authored worker requirements.:job-typeis accepted as a single-type alias when only one job type is needed.
Setup Forms And Invocation Inputs
Use :requires for setup values saved on the configured target. Use
:invocations for values supplied each time the flow is run.
Setup form example:
{:requires [{:kind :form
:label "Setup"
:fields [{:key :region
:label "Region"
:field-type :select
:required true
:options ["EU" "US"]}]}]}
Per-run input example:
{:invocations {:default
{:label "Run input"
:inputs [{:name :question
:label "Question"
:type :text
:required true}]}}
:interfaces {:manual [{:id :ask
:label "Ask"
:invocation :default
:enabled true}]}
:flow
'(let [input (flow/input)]
{:region (:region input)
:question (:question input)})}
The flow sees setup values and invocation inputs together in flow/input.
Setup values persist until reconfigured. Invocation inputs apply only to the
current run.
Setup Form Requirements
Use {:kind :form ...} inside :requires when the flow needs structured setup
input before it can run.
Top-level setup form 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 UI. |
:provided-by | No | :installer (default) or :author. Author-provided requirements are not shown to the installer. |
:collect | No | Deprecated for new source. Omit it for setup; :setup remains the default for old examples. |
Setup field keys:
| Key | Required | Meaning |
|---|---|---|
:key, :name, or :id | 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 | Static allowed values. |
:options-source | For :select | Dynamic option source metadata. Use when options come from platform services, such as items visible through a bound connection. |
: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 setup :field-type values:
:field-type | UI behavior |
|---|---|
:text | Single-line text input |
:textarea | Multi-line text box |
:select | Dropdown |
:boolean | Checkbox/toggle |
:number | Numeric input |
:date | Date picker |
:time | Time picker |
:datetime | Date-time picker |
Invocation Inputs
Declare fields under :invocations when the value is supplied per run.
Run input validation normalizes declared :number fields before the flow starts,
so function steps should receive numbers for those fields.
Invocation input keys:
| Key | Required | Meaning |
|---|---|---|
:name, :key, or :id | Yes | Stable submitted field id. The flow reads this key from flow/input. |
:type | No | Input type. Defaults to text-like behavior when omitted. |
:label | No | Display label in UI. |
:description | No | Help text for the input. |
:required | No | When true, the field must be supplied before the run starts. |
:placeholder | No | Helper text shown in the input UI. |
:default | No | Default value shown when the run form first loads. |
:options | For :select | Static allowed values. |
:options-source | For :select | Dynamic option source metadata. Use when options come from platform services, such as items visible through a bound connection. |
:multiple | For :file, :blob-ref, or :resource | Allow multiple selected/uploaded values. |
:resource-types | For :resource | Optional resource type filter such as [:file :result]. |
:accept | For :file, :blob-ref, or :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". |
Dynamic select option sources:
:optionsremains the static select option list.:options-sourcedeclares platform-owned option discovery for cases where
choices depend on an installation binding or setup state.- The first supported source type is
:connection-items, which reads
non-secret cached item metadata for the selected connection binding. - Option hydration and validation read Breyta's connection-owned item cache. It
does not run arbitrary author code or make provider API calls from the form
renderer or setup/run save path. - Setup and run submissions fail closed when the backing connection is missing,
the cache is empty, the selected item is disabled, or the submitted value is
not in the cached options. - This validation is scoped to the declared connection-backed field. It is not a
general connection health check, and connection health/readiness checks remain
best-effort operational signals. - Declare either
:slotor:connectionas the local connection slot reference.
Prefer:slot;:connectionis accepted as a compatibility alias. Do not set
both unless they refer to the same slot. :item-typeis a connector-defined category such as"project",
"channel","account","repository", or a provider-qualified variant
such as"linear/project"or"slack/channel".:item-typeis required and is matched as a string, so provider-qualified
values such as"github/repository"keep their slash.:value-field,:label-field, and:description-fieldoptionally map
provider item payload fields into dropdown values. When omitted, the renderer
falls back tovalue/idfor the submitted value,labelfor display text,
anddescriptionfor helper text.- The submitted value is still a normal scalar form value; the option source
is a UX and validation hint, not a runtime connection handle. - Runtime still validates access when the flow uses the selected value, because
connector authorization can change after picker metadata is cached.
Example:
{:requires [{:slot :work-system
:type :http-api
:label "Work system"
:provided-by :installer}]
:invocations {:default
{:inputs [{:name :target-project
:label "Project"
:type :select
:required true
:options-source {:type :connection-items
:slot :work-system
:item-type "project"
:value-field "id"
:label-field "name"}}]}}}
Supported invocation :type values:
:type | UI behavior |
|---|---|
:text or :string | Single-line text input |
:textarea | Multi-line text box |
:select | Dropdown |
:boolean or :checkbox | Checkbox/toggle |
:number | Numeric input |
:date | Date picker |
:time | Time picker |
:datetime | Date-time picker |
:file, :blob, or :blob-ref | Upload file input |
:resource | Select existing workspace resources or upload local files |
:secret, :password, :email | Text inputs with specialized semantics/UI where supported |
Manual interface upload inputs quick reference:
- use
:file,:blob, or:blob-refwhen the run form should accept a new
local upload for that run - use
:resourcewhen the run form should select existing workspace resources
and optionally allow local uploads into that picker - use
:multiple truewhen the submitted value should be a collection - use
:accept [...]to filter by MIME type for both browser file selection and
resource picker results - uploaded files are passed to the flow as blob/resource reference maps, not
inline bytes; downstream steps should read or pass those refs explicitly
Direct file upload example:
{:invocations {:default
{:label "Upload receipts"
:inputs [{:name :receipts
:label "Receipts"
:type :file
:required true
:multiple true
:accept ["application/pdf"
"image/*"]}]}}
:interfaces {:manual [{:id :upload-receipts
:label "Upload receipts"
:invocation :default
:enabled true}]}}
The flow reads the uploaded refs from flow/input:
'(let [{:keys [receipts]} (flow/input)]
{:receipt-count (count receipts)
:receipt-refs receipts})
Resource input example:
{:requires [{:slot :archive
:type :blob-storage
:label "Archive storage"
:config {:prefix {:default "reports"
:label "Folder prefix"
:placeholder "reports/customer-a"}}}]
:invocations {:default
{:label "Run input"
:inputs [{:name :question
:label "Question"
:type :text
:required true}
{:name :resources
:label "Resources"
:type :resource
:slot :archive
:required true
:multiple true
:accept ["application/pdf" "text/plain"]}]}}
:interfaces {:manual [{:id :ask
:label "Ask"
:invocation :default
:enabled true}]}}
Resource input expectations:
- selected values are passed as resource references, not full file contents
- use
:multiple truewhen the flow expects a collection of resources - local uploads are saved as workspace
:fileresources and auto-selected for the current run :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- persisted table resources are
:resultresources with content typeapplication/vnd.breyta.table+json - use
:resource-types [:result]and optionally:accept ["application/vnd.breyta.table+json"]when the picker should show only table resources - 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
Table-resource input example:
{:invocations {:default
{:inputs [{:name :source-table
:label "Source table"
:type :resource
:resource-types [:result]
:accept ["application/vnd.breyta.table+json"]}]}}}
Text-focused resource allowlist:
{:name :resources
:label "Text resources"
:type :resource
:required true
:multiple true
:accept ["text/*"
"application/json"
"application/xml"
"application/edn"]}
Example input seen by the flow:
{: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"}]}
Blob Storage Slots And Resource Pickers
Blob-storage authoring lives in :requires because the installer configures
the storage binding once. Per-run pickers live in :invocations because the
selected resource changes on each run.
{:requires [{:slot :archive
:type :blob-storage
:label "Archive storage"
:config {:prefix {:default "reports"}}}]
:invocations {:default
{:inputs [{:name :report
:label "Archived report"
:type :resource
:required true
:accept ["application/pdf"]
:slot :archive}]}}}
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/<installation-id>/reportswhen no 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
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.
Deprecated Run Form Shapes
These forms are still accepted so existing flows keep working, but do not use
them in new source.
- Run forms previously collected from requirements should move their fields to
:invocations. - Manual launch fields should move to
:invocationsand be exposed through
:interfaces :manual. - Time automation should move to top-level
:schedules. - Webhook ingress should move payload fields to
:invocationsand expose the
webhook through:interfaces :webhook.
When a legacy flow has no explicit :invocations, Breyta derives a default
invocation from older run field shapes. Legacy manual, schedule, and webhook
definitions remain accepted, but they are not the preferred canonical
invocation/interface/schedule source. Adding an explicit :invocations map
makes that map the source of truth, and adding :interfaces {:webhook [...]}
makes the interface map the source of webhook ingress truth.
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
:fanout is a step, not a non-step orchestration form.
Use flow/step :fanout when you need bounded child-workflow batch spawn/collect, and see Step Fanout for its guardrails and result shape.
:job is also a step, not a non-step orchestration form.
Use flow/step :job when you need to submit, read, or await external jobs in
the Breyta jobs control plane, and see Step Job
for the step contract.
Common Orchestration Shape
Use one orchestration layer in :flow, then delegate:
- shaping/normalization to
:functions - large static bodies/prompts/queries to
:templates - heavy built-in step configs behind simplified packaged
:steps - reusable agent roles and delegation policy to top-level
:agents - heavy outputs to
:persist - external worker execution to
flow/step :job - larger branches to child flows via
flow/call-flow - bounded child-workflow batch spawn/collect to
:fanoutwith{:type :call-flow ...}items
{:functions [{:id :prepare
:language :clojure
:code "(fn [input] {:order-id (:order-id input) :lookup-key (str \"orders:\" (:order-id input))})"}]
:steps [{:id :billing/fetch-order
:type :http
:description "Fetch an order from the billing API."
:input-schema [:map [:order-id :string]]
:defaults {:connection :orders-api
:template :fetch-order}
:prepare :prepare}]
:flow
'(let [input (flow/input)
order (flow/step :billing/fetch-order :fetch-order
{:order-id (:order-id input)
:persist {:type :blob}})
route (flow/call-flow :order-routing
{:order-ref (:uri order)
:lookup-key (str "orders:" (:order-id input))})
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 (:uri order)
:route route
:result result})}
Packaged Steps
Top-level :steps let you define flow-local packaged step wrappers around
heavy built-in step configs.
Rules:
- packaged step ids must be qualified keywords such as
:github/open-pr - unqualified keywords remain reserved for built-in step families like
:http,
:files, and:table - packaged steps wrap one underlying built-in step
- use top-level
:functionsfor:prepareand:project-resulthelpers
Canonical shape:
{:steps [{:id :github/open-pr
:type :http
:description "Open a draft GitHub pull request."
:input-schema [:map
[:owner :string]
[:repo :string]
[:title :string]
[:head :string]
[:base :string]]
:output-schema [:map [:url :string]]
:defaults {:connection :github-api
:template :open-pr-template}
:prepare :prepare-open-pr
:project-result :project-open-pr
:tool {:name "github_open_pr"
:description "Open a draft GitHub pull request."}}]}
Direct invocation uses the packaged step id directly in flow/step:
'(flow/step :github/open-pr :open-pr
{:owner "breyta"
:repo "breyta"
:title "Fix null handling"
:head "task/fix-null"
:base "main"})
How defaults and helpers are resolved:
:defaultsmerge into the wrapped built-in step config:prepareand:project-resultresolve from top-level:functions- flow-local
:templatesand default:connectionvalues are resolved by the
wrapped built-in step at runtime - the same resolution applies when a packaged step is published as an
agent tool through:tools {:steps [...]}; the tool call still runs through
the wrapped built-in step with those packaged defaults applied - common outer step controls like
:retry,:timeout,:persist, and
:on-errorstill belong on the outerflow/stepcall, not in the packaged
step definition
When one parent needs several sibling child workflows, keep the batch bounded and use :fanout with :call-flow items only.
Legacy non-child fanout remains available for compatibility, but it runs on the sequential compatibility path.
Agent Definitions
Top-level :agents define named, reusable agent configurations. Each agent
definition declares its objective, tools, connection, iteration budget, and
optional memory in one place.
Agent definition fields:
| Field | Type | Required | Notes |
|---|---|---|---|
:id | qualified keyword | Yes | e.g. :review/security, :fix/single-area |
:description | string | Yes | Human-readable description (max 4000 chars) |
:objective | string | Yes | The agent's objective template |
:instructions | string | No | Additional guidance appended to system prompt |
:input-schema | schema | No | Validates invocation input before execution |
:output-schema | schema | No | Validates agent output (warning only) |
:connection | keyword/string | No | Default LLM connection slot |
:model | string | No | Default model |
:available-steps | vector | No | Built-in step tools (:files, :table, :search only) |
:tools | map | No | Full tools config including :steps [...] and :agents [...] |
:max-iterations | int | No | Iteration cap; default 20, max 100 |
:max-tool-calls | int | No | Total executed tool-call cap; default 100, max 1000 |
:max-repeated-tool-calls | int | No | Optional cap for identical tool name+arguments calls; unset by default, max 100 |
:memory | map | No | Memory table config for cross-run context |
:output | map | No | Output format (e.g. {:format :json}) |
:tool | map | No | Tool publication metadata (:name, :description) |
Rules:
- agent ids must be qualified keywords such as
:review/security - each agent definition IS an
:agentstep config — no wrapping needed - invocation input is validated against
:input-schemaand becomes:inputs - agents can be published as tools for other agents via
:tools {:agents [...]}
Canonical shape:
{:agents [{:id :review/security
:description "Scan code for security issues."
:objective "Scan the selected area for OWASP top 10 issues."
:instructions "Focus on injection, auth bypass, and data leaks."
:connection :ai
:model "gpt-5-mini"
:available-steps [:files :table]
:max-iterations 20
:max-tool-calls 80
:max-repeated-tool-calls 3
:input-schema [:map [:area :string] [:repo-tree :any]]
:output {:format :json}
:output-schema [:map [:findings [:vector :string]]]
:tool {:name "security_scan"
:description "Run a security scan on a code area."}}]
:flow
'(let [result (flow/step :review/security :scan-auth
{:area "auth" :repo-tree repo-tree})]
(:findings result))}
Invocation:
- everything in the invocation config that isn't an overlay key becomes agent
:inputs - overlay keys (
:connection,:model,:max-iterations,:max-tool-calls,
:max-repeated-tool-calls,:memory) override defaults - the agent runs a full agentic loop with its own iteration budget and tools
Agent-to-agent delegation:
'(flow/step :agent :coordinator
{:connection :ai
:objective "Review the repo, then delegate scans."
:tools {:agents [:review/security]
:allowed ["files"]}
:max-iterations 10})
The coordinator sees security_scan as a tool. When called, the sub-agent
runs a nested agentic loop with its own budget, and the projected result
returns to the coordinator as tool output.
Providers
Top-level :providers registers named provider definitions for step families.
Use :providers {:llm [...]} for custom LLM endpoints that :llm and
:agent steps can use via :provider. Use :providers {:files [...]} for
custom repository/source providers that :files steps can use via :provider.
Only the :llm and :files provider families are accepted; unknown family keys
are rejected during flow validation so typos do not silently disappear.
LLM family-based providers
Reuse an existing provider family with custom defaults:
{:providers
{:llm [{:id :fireworks-deepseek-r1
:family :chat-completions-compatible
:defaults {:base-url "https://api.fireworks.ai/inference/v1"
:model "accounts/fireworks/models/deepseek-r1"}}
{:id :local-ollama
:family :chat-completions-compatible
:defaults {:base-url "http://localhost:11434/v1"
:model "llama3.1"}}]}
:flow
'(flow/step :llm :summarize
{:connection :ai
:provider :fireworks-deepseek-r1
:prompt "Summarize this document."})}
Supported families: :openai (Responses API), :deepseek (DeepSeek Chat
Completions with agentic tool-loop support), :chat-completions-compatible
(generic Chat Completions), :openai-compatible (legacy alias for
:chat-completions-compatible), :openrouter (OpenRouter Chat Completions),
:anthropic, :bedrock (AWS Bedrock Runtime for Anthropic Claude with
SigV4), :google, and :custom.
The provider inherits all capabilities from its family. :defaults are
merged under the step config — the step can still override :model,
:temperature, etc.
The built-in :chat-completions-compatible family intentionally does not
declare :agentic-tool-loop by default. It covers endpoints that share the
Chat Completions request/response shape, but that does not guarantee multi-turn
tool-loop behavior. If you have verified that a Chat Completions-like endpoint
can replay assistant tool_calls, accept subsequent role=tool messages, and
round-trip tool call ids for the chosen model, opt in explicitly:
{:providers
{:llm [{:id :agentic-compatible-llm
:family :chat-completions-compatible
:defaults {:base-url "https://llm.example.com/v1"
:model "provider/model"}
:capabilities #{:tool-calling
:structured-output
:agentic-tool-loop}}]}}
This capability override is author-owned compatibility, not a platform
guarantee for every model behind that endpoint.
Do not use a custom provider id that matches a built-in provider id such as
:deepseek, unless you intentionally want the flow-local provider to shadow
the built-in provider during that flow execution.
Custom LLM Providers
For endpoints that don't speak a standard API, route calls through a
packaged step:
{:providers
{:llm [{:id :internal-llm
:family :custom
:call-step :internal/call-llm}]}
:steps
[{:id :internal/call-llm
:type :http
:description "Call internal LLM API"
:input-schema [:map [:messages [:vector :any]]]
:defaults {:connection :internal-api
:method :post
:path "/v1/completions"}
:prepare :prepare-llm-request
:project-result :project-llm-response}]
:functions
[{:id :prepare-llm-request
:language :clojure
:code "(fn [{:keys [messages model]}]
{:json {:messages messages :model (or model \"default\")}
:response-as :json})"}
{:id :project-llm-response
:language :clojure
:code "(fn [result]
{:success true
:content (get-in result [:body :choices 0 :message :content])
:usage {:prompt-tokens (get-in result [:body :usage :prompt_tokens])
:completion-tokens (get-in result [:body :usage :completion_tokens])
:total-tokens (get-in result [:body :usage :total_tokens])}})"}]}
The :project-result function must return the canonical result envelope:
{:success true :content "..." :usage {:prompt-tokens N ...}}.
Custom Files Providers
Custom files providers route repository operations through packaged steps:
{:providers
{:files [{:id :gitlab
:display-name "GitLab"
:operations {:load-source-entries :gitlab/load-source
:publish-changes :gitlab/publish
:open-change-request :gitlab/open-change-request}}]}
:steps
[{:id :gitlab/load-source
:type :http
:description "Load GitLab repository entries"
:input-schema [:map [:repo :string]]
:defaults {:connection :gitlab-api
:method :get
:path "/projects/{{repo}}/repository/tree"}}
{:id :gitlab/publish
:type :http
:description "Publish GitLab branch changes"
:input-schema [:map [:repository :string] [:branch :string]]
:defaults {:connection :gitlab-api
:method :post
:path "/projects/{{repository}}/repository/commits"}}
{:id :gitlab/open-change-request
:type :http
:description "Open GitLab merge request"
:input-schema [:map [:repository :string] [:branch :string] [:title :string]]
:defaults {:connection :gitlab-api
:method :post
:path "/projects/{{repository}}/merge_requests"}}]}
LLM provider definition fields
| Field | Type | Required | Notes |
|---|---|---|---|
:id | keyword | Yes | Used as :provider value in step config |
:family | keyword | Yes | :openai, :deepseek, :chat-completions-compatible, :openai-compatible legacy alias, :openrouter, :anthropic, :google, or :custom |
:defaults | map | No | Merged under step config (base-url, model, headers) |
:call-step | keyword | For :custom | Qualified packaged step id |
:display-name | string | No | Human-readable name |
:capabilities | set | No | Overrides family capabilities if set; use explicit :agentic-tool-loop only for endpoints/models verified to support multi-turn tool replay |
Files provider definition fields
| Field | Type | Required | Notes |
|---|---|---|---|
:id | keyword | Yes | Used as :provider value in :files step config |
:display-name | string | No | Human-readable provider name |
:operations | map | Yes | Maps supported files operations to qualified packaged step ids |
:operations :load-source-entries | keyword | Yes | Packaged step used to load source/repository entries |
:operations :publish-changes | keyword | Yes | Packaged step used to publish a changeset |
:operations :open-change-request | keyword | Yes | Packaged step used to open a pull/merge/change request |
Connection handling
The provider definition does not replace connection bindings. The :connection
on the :llm step still resolves through :requires bindings. The provider
only determines the API format and endpoint — the connection provides the
credentials.
For installable flows, use :provided-by :author on the connection when the
author wants to absorb LLM costs, regardless of which provider is configured.
Local Source Includes
When you author a flow locally for breyta flows push --file ..., the CLI can expand explicit local include forms before upload:
{:templates [#flow/include "flow-assets/templates/security-reviewer.edn"]
:functions [#flow/include "flow-assets/functions/normalize-config.edn"]
:steps [#flow/include "flow-assets/steps/github-open-pr.edn"]
:flow '(let [review-schema #flow/include "flow-assets/review-schema.edn"]
review-schema)}
Rules:
#flow/include "relative/path"resolves relative to the flow source file being pushed.- Expansion happens client-side in the CLI before
flows.put_draft; the server still receives a normalflowLiteral. - Included files can include other files recursively through the same form.
- Cycles fail the push.
- The authored source file is not rewritten with inlined content during push.
Recommended use:
- large
:templates - large
:functions - reusable packaged
:steps - adjacent JSON-schema / EDN config fragments used in
:flow
This is an authoring convenience for local source files. Pulled flow source still materializes the resolved content rather than reconstructing your local include tree.
: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 (:uri report-a)
:slot-managed (:uri 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
Built-In Sandbox Helpers
These functions are available directly in :flow code and :functions without
requiring imports:
| Helper | Description |
|---|---|
(json-parse s) | Parse JSON string → keyword map, returns nil on invalid input |
(json-parse! s) | Parse JSON string → keyword map, throws on invalid input |
(json-emit value) | Serialize Clojure data → JSON string |
(re-find pattern s) | First regex match (pattern can be string or regex) |
(re-matches pattern s) | Full-string regex match |
(re-seq pattern s) | All matches (bounded by sandbox limit) |
(group-by f coll) | Group collection by function (bounded) |
(frequencies coll) | Count occurrences (bounded) |
(present? value) | Not nil, not blank, not empty |
(blank? value) | Nil, empty string, or empty collection |
Also available: clojure.string/* (via str/), clojure.set/* (via set/),
and breyta.sandbox/* for base64, hex, SHA-256, UUID, instant parsing, and
URL encoding.
Common pattern with agent output:
'(let [result (flow/step :agent :review {:objective "..." :output {:format :json}})
findings (or (:content result)
(json-parse (:content result)))]
(group-by :severity (:findings findings)))
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