Docs
Reference

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

FieldRequiredPurposeNotes
:slugYesStable flow identifierUsed by CLI/API commands.
:nameYesHuman-readable display nameSafe to change without changing slug identity.
:concurrencyYesOverlap/version execution policyDefines overlap rules and version behavior.
:flowYesDeterministic orchestration bodyKeep orchestration-focused; move shaping/transforms to top-level :functions.
:descriptionNoOperator/user contextSanitized Markdown on setup/run pages.
:deploy-key-requiredNoProtect release/promotion operations behind a shared deploy keyStored as flow metadata; when true, guarded release/promotion operations require a valid key.
:requiresNoSetup dependenciesConnections/secrets/setup/worker requirements. Per-run fields belong in :invocations.
:invocationsNoPer-run input contractsInputs supplied by users, webhooks, CLI, or clients. Use :default for normal flows.
:interfacesNoCallable ingress surfacesManual, HTTP, webhook, or MCP surfaces over invocation contracts.
:schedulesNoTime-based automationDeclares cron/preset schedules that call an invocation. New scheduled automation belongs here.
:templatesNoReusable large static contentBest for request bodies, prompts, queries, notifications.
:functionsNoReusable Clojure transformsCall via flow/step :function refs from :flow.
:stepsNoFlow-local packaged stepsQualified ids only, for example :github/open-pr.
:agentsNoFlow-local named agent definitionsQualified ids only, for example :review/security. Reusable agent configurations invocable from flow/step and publishable as tools for other agents.
:providersNoFlow-local provider definitionsRegister custom LLM and files providers under {:llm [...] :files [...]}. Referenced via :provider in :llm, :agent, and :files steps.
:mcpNoFlow-level MCP tool adaptersReuses :http-api connection slots with :backend :mcp; adapters allowlist server tools and publish selected tools via :tools {:mcp [...]}.
:mcp-registriesNoOptional MCP registry metadataAuthoring metadata for known remote MCP endpoints; runtime exposure still requires explicit entries in :mcp and step-local :tools.
:tagsNoClassification/feature tagsOptional 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-resource blocks
  • 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 :raw viewer

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:

  1. :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 / :agents around those slots
  • if a packaged tool wraps a broad connection-backed surface, add flow-local permission hints here before publishing it to an agent
  1. :invocations
  • declare per-run fields that a user, CLI command, interface, schedule, or client provides
  • use :default for a single run form
  • point manual interfaces at the contract with :invocation :default
  • put webhook ingress under :interfaces :webhook and point it at the same
    contract with :invocation :default
  1. :interfaces
  • declare manual, HTTP, webhook, and MCP call surfaces over invocations
  • use :manual for Resource UI launch forms and "run now" actions
  • use :webhook for inbound provider events with explicit auth
  • use :http and :mcp for programmatic clients and coding agents
  1. :schedules
  • declare cron/preset automation over invocations
  • use stable schedule ids because setup overrides and CLI toggles address
    schedules by id
  1. :templates
  • move prompts, request bodies, SQL, notification content, and other large
    static text here
  1. :functions
  • put shaping, normalization, preparation, and projection logic here
  1. :steps
  • package heavy built-in step configs behind a narrower input/output contract
  • especially for broad surfaces like :http, :db, :notify, :function,
    and :agent
  1. :agents
  • define reusable named agent roles here when the behavior is agent-shaped
  • capture objective, instructions, memory, cost/evaluate/trace, and delegation
    config once
  1. :mcp when using remote MCP tools
  • bind the remote endpoint as a normal :http-api requirement 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 / :agent steps via
    :tools {:mcp [...]}
  1. :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:

  • :files for code/resource state
  • packaged :steps for heavy external or policy-shaped operations
  • named :agents for reviewer/fixer/coordinator roles
  • orchestration last

For installable flows, the authoring order matters more because it determines
the author/installer boundary:

  • :requires defines 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).
  • :steps and :agents reference connection slots, not IDs. The slot name
    resolves through :requires bindings at runtime, so it works in both the
    author's workspace and any installation.
  • :memory, :cost, :trace, :evaluate are internal to the
    flow — the installer never sees these resources.
  • :output-persist writes 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:

FieldBehavior
SchedulesRender schedule controls inside setup.
Saved choicesStored as installation schedule settings. New installed schedules start disabled until enabled.
Author setupAuthor overrides use the same controls. Reset restores authored defaults.
Time/day overridesDaily/weekly/monthly cadences expose bounded time/day controls. Monthly last uses a guarded 28-31 cron window.
Timezone controlSaved/default zones win; detected zones fill gaps.
Source schedule configSetup does not rewrite authored :cron or :timezone; schedule sync/runtime consumes saved schedule settings for installer-specific schedules.
Automatic execution stateInstalled flows use per-schedule and per-interface enablement.
Webhook interface setupSetup 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 true requires a valid deploy key for guarded release operations (for example flows release and flows promote).
  • Disabling the guard (:deploy-key-required false) also requires a valid deploy key.
  • Enabling the guard fails unless BREYTA_FLOW_DEPLOY_KEY is 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:

Aspectdraft targetlive target
Definition sourceLatest pushed working copy in workspace.Installed released version.
How target changesbreyta flows push --file ...breyta flows release <slug> or breyta flows promote <slug>
:requires resolutionbreyta flows configure <slug> ... values.breyta flows configure <slug> --target live ... or installation-specific configuration values.
Webhook interface endpoint surfaceFlow-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 runbreyta flows run <slug> --waitbreyta flows run <slug> --target live --wait
Best useAuthoring 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 :requires for both setup inputs and external worker/runtime dependencies.
  • Use :provided-by :author when the flow author supplies the worker/runtime.
  • Use :provided-by :installer when 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-types in authored worker requirements. :job-type is 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:

KeyRequiredMeaning
:kindYesMust be :form.
:fieldsYesOne field map or a vector of field maps.
:labelNoGroup label shown in setup UI.
:provided-byNo:installer (default) or :author. Author-provided requirements are not shown to the installer.
:collectNoDeprecated for new source. Omit it for setup; :setup remains the default for old examples.

Setup field keys:

KeyRequiredMeaning
:key, :name, or :idYesStable submitted field id.
:labelNoDisplay label in UI.
:field-typeNoInput type. Defaults to :text.
:requiredNoWhen true, the field must be filled before save/run.
:placeholderNoHelper text shown in the input UI.
:defaultNoDefault value shown when the form first loads.
:optionsFor :selectStatic allowed values.
:options-sourceFor :selectDynamic option source metadata. Use when options come from platform services, such as items visible through a bound connection.
:multipleFor :resourceAllow selecting more than one resource.
:resource-typesFor :resourceOptional resource type filter such as [:file :result].
:acceptFor :resourceOptional MIME filter such as ["application/pdf" "text/plain" "text/*"].
:slotFor :resourceOptional local blob-storage slot such as :archive.
:storage-backendFor :resourceOptional explicit storage backend filter such as :gcs. Usually inferred from :slot when present.
:storage-rootFor :resourceOptional explicit storage root such as "reports/acme". Usually inferred from :slot when present.
:path-prefixFor :resourceOptional relative path prefix under the effective storage root, such as "exports/2026".
:sourceFor :resourceLegacy picker routing override. Omit in normal flow authoring.

Supported setup :field-type values:

:field-typeUI behavior
:textSingle-line text input
:textareaMulti-line text box
:selectDropdown
:booleanCheckbox/toggle
:numberNumeric input
:dateDate picker
:timeTime picker
:datetimeDate-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:

KeyRequiredMeaning
:name, :key, or :idYesStable submitted field id. The flow reads this key from flow/input.
:typeNoInput type. Defaults to text-like behavior when omitted.
:labelNoDisplay label in UI.
:descriptionNoHelp text for the input.
:requiredNoWhen true, the field must be supplied before the run starts.
:placeholderNoHelper text shown in the input UI.
:defaultNoDefault value shown when the run form first loads.
:optionsFor :selectStatic allowed values.
:options-sourceFor :selectDynamic option source metadata. Use when options come from platform services, such as items visible through a bound connection.
:multipleFor :file, :blob-ref, or :resourceAllow multiple selected/uploaded values.
:resource-typesFor :resourceOptional resource type filter such as [:file :result].
:acceptFor :file, :blob-ref, or :resourceOptional MIME filter such as ["application/pdf" "text/plain" "text/*"].
:slotFor :resourceOptional local blob-storage slot such as :archive.
:storage-backendFor :resourceOptional explicit storage backend filter such as :gcs. Usually inferred from :slot when present.
:storage-rootFor :resourceOptional explicit storage root such as "reports/acme". Usually inferred from :slot when present.
:path-prefixFor :resourceOptional relative path prefix under the effective storage root, such as "exports/2026".

Dynamic select option sources:

  • :options remains the static select option list.
  • :options-source declares 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 :slot or :connection as the local connection slot reference.
    Prefer :slot; :connection is accepted as a compatibility alias. Do not set
    both unless they refer to the same slot.
  • :item-type is a connector-defined category such as "project",
    "channel", "account", "repository", or a provider-qualified variant
    such as "linear/project" or "slack/channel".
  • :item-type is required and is matched as a string, so provider-qualified
    values such as "github/repository" keep their slash.
  • :value-field, :label-field, and :description-field optionally map
    provider item payload fields into dropdown values. When omitted, the renderer
    falls back to value/id for the submitted value, label for display text,
    and description for 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:

:typeUI behavior
:text or :stringSingle-line text input
:textareaMulti-line text box
:selectDropdown
:boolean or :checkboxCheckbox/toggle
:numberNumeric input
:dateDate picker
:timeTime picker
:datetimeDate-time picker
:file, :blob, or :blob-refUpload file input
:resourceSelect existing workspace resources or upload local files
:secret, :password, :emailText inputs with specialized semantics/UI where supported

Manual interface upload inputs quick reference:

  • use :file, :blob, or :blob-ref when the run form should accept a new
    local upload for that run
  • use :resource when the run form should select existing workspace resources
    and optionally allow local uploads into that picker
  • use :multiple true when 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 true when the flow expects a collection of resources
  • local uploads are saved as workspace :file resources and auto-selected for the current run
  • :accept matches resource MIME types; use exact values like "application/pdf" or wildcard prefixes like "text/*"
  • :resource-types and :accept are combined, so a resource must satisfy both filters when both are present
  • persisted table resources are :result resources with content type application/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 :slot points 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 as installations/<installation-id>/reports when no explicit root is saved
  • another flow shares only when its own local slot is explicitly configured to the same concrete storage location
  • :prefers on a blob-storage slot records intended upstream sharing, but it does not auto-select or persist the consumer root
  • top-level :connections remains 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 :invocations and be exposed through
    :interfaces :manual.
  • Time automation should move to top-level :schedules.
  • Webhook ingress should move payload fields to :invocations and 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:

CategoryConstructsTypical Use
Binding / sequencinglet, doBuild deterministic orchestration pipelines.
Branchingif, if-not, when, when-not, cond, caseRoute execution by state or decision flags.
Iterationfor, doseq, loop, recurDeterministic fan/loop orchestration.
Child flow orchestrationflow/call-flowDelegate larger branches to child flows.
Polling helperflow/pollBounded 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"} for if/if-not
  • ^{:label "Retry Loop" :max-iterations 5} for loop

: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 :fanout with {: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 :functions for :prepare and :project-result helpers

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:

  • :defaults merge into the wrapped built-in step config
  • :prepare and :project-result resolve from top-level :functions
  • flow-local :templates and default :connection values 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-error still belong on the outer flow/step call, 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:

FieldTypeRequiredNotes
:idqualified keywordYese.g. :review/security, :fix/single-area
:descriptionstringYesHuman-readable description (max 4000 chars)
:objectivestringYesThe agent's objective template
:instructionsstringNoAdditional guidance appended to system prompt
:input-schemaschemaNoValidates invocation input before execution
:output-schemaschemaNoValidates agent output (warning only)
:connectionkeyword/stringNoDefault LLM connection slot
:modelstringNoDefault model
:available-stepsvectorNoBuilt-in step tools (:files, :table, :search only)
:toolsmapNoFull tools config including :steps [...] and :agents [...]
:max-iterationsintNoIteration cap; default 20, max 100
:max-tool-callsintNoTotal executed tool-call cap; default 100, max 1000
:max-repeated-tool-callsintNoOptional cap for identical tool name+arguments calls; unset by default, max 100
:memorymapNoMemory table config for cross-run context
:outputmapNoOutput format (e.g. {:format :json})
:toolmapNoTool publication metadata (:name, :description)

Rules:

  • agent ids must be qualified keywords such as :review/security
  • each agent definition IS an :agent step config — no wrapping needed
  • invocation input is validated against :input-schema and 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

FieldTypeRequiredNotes
:idkeywordYesUsed as :provider value in step config
:familykeywordYes:openai, :deepseek, :chat-completions-compatible, :openai-compatible legacy alias, :openrouter, :anthropic, :google, or :custom
:defaultsmapNoMerged under step config (base-url, model, headers)
:call-stepkeywordFor :customQualified packaged step id
:display-namestringNoHuman-readable name
:capabilitiessetNoOverrides family capabilities if set; use explicit :agentic-tool-loop only for endpoints/models verified to support multi-turn tool replay

Files provider definition fields

FieldTypeRequiredNotes
:idkeywordYesUsed as :provider value in :files step config
:display-namestringNoHuman-readable provider name
:operationsmapYesMaps supported files operations to qualified packaged step ids
:operations :load-source-entrieskeywordYesPackaged step used to load source/repository entries
:operations :publish-changeskeywordYesPackaged step used to publish a changeset
:operations :open-change-requestkeywordYesPackaged 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 normal flowLiteral.
  • 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 :slot writes under the runtime-managed base workspaces/<ws>/persist/<flow>/<step>/<uuid>/...
  • :persist {:type :blob :slot :archive ...} writes under the installer-bound storage base workspaces/<ws>/storage/<root>/...
  • In both modes, :persist :path stays relative to that base and :filename stays the leaf name
  • Use :slot only 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:

HelperDescription
(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/step boundaries
  • 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 :persist as a common default for variable-size step outputs

See limits: Limits And Recovery

Related

As of May 21, 2026