Docs
Playbooks

Playbook: Author Flows

Load when creating or editing a Breyta flow, adding steps, changing inputs,
copying template patterns, or preparing a draft for release.

Mental Model

Flow definitions are a Breyta DSL with Clojure/EDN syntax. They are not normal
Clojure programs. The DSL describes an orchestration graph plus contracts:
:requires, :invocations, :interfaces, :concurrency, reusable
:templates, :functions, packaged :steps, :agents, and finally :flow.

Author with platform limits in mind: deterministic orchestration, explicit step
boundaries, serializable maps/vectors, bounded loops, persisted large payloads,
and live/install targets that may differ from draft.

flow/step takes exactly three arguments: step type or packaged id, step id,
and one config map. Outer controls such as :on-error, :retry, :timeout,
and :persist are keys inside that config map, never a fourth argument.

For first proof, inline small transform functions and step bodies when that is
the shortest path to a green draft run. Extract repeated or bulky pieces into
top-level :functions, :templates, or packaged :steps after the first proof,
before release or marketplace review.

Public App Intent Gate

When the requested outcome is a public app (installable, Discover, or
marketplace), design the installer's setup screen before any flow source:

  • target at most one connection card per provider account and origin plus at
    most one required text field; reject any :requires list where two slots
    share a provider account and origin
  • public unauthenticated endpoints need no connection slot; :http with a
    plain :url works for public GETs such as sitemaps and feeds
  • declare one connection per provider account and origin; prefer a
    bare-origin :base-url with path prefixes in step :path, and never
    split slots by path prefix on the same origin. Distinct API hosts need
    separate slots; keep provider-required prefixes, such as a self-managed
    GitLab /api/v4 :base-url, when a provider-backed step's contract
    demands them
  • derive the flow from the approved setup screen, not the setup screen from
    the flow
  • load references/public-flows.md (setup screen, degradation contract) and
    playbooks/public-and-marketplace.md before writing source

Workspace-internal flows keep the normal loop below. This gate applies only
when public-app intent is stated or becomes apparent.

Default Loop

  1. Start a small session capsule:
    • workspace, flow slug, task mode
    • intended interface and target
    • refs consulted
    • chosen pattern
    • next proof command
    • risks
  2. Reuse before writing:
    • breyta flows search "<integration or pattern>" --limit 5
    • breyta flows grep "<literal>" --surface definition,tools --limit 5 when metadata is not enough
    • breyta flows templates search "<problem>" --limit 5
    • breyta flows templates grep "<step or config>" --surface steps,tools --limit 5 when template metadata is not enough
    • breyta resources search "<existing data>" --limit 5 when prior uploads, reports, or run outputs may be reused
    • treat hits like rg: carry forward hitRef, matched field/surface,
      snippet/preview, and nextCommand; open one focused target, not every hit
    • for existing-flow edits, inspect the current flow before comparing examples
    • in clearly empty hosted workspaces, skip proving absence and start from the requested outcome
  3. Define the contract before source grows:
    • :requires
    • :invocations
    • :interfaces
    • :concurrency
    • output shape
    • side effects and failure behavior
  4. For a new flow, run breyta flows init <slug> --name "...". It creates the
    local canonical .clj source with one no-input manual run interface and
    an empty :schedules vector. For an existing flow, use breyta flows pull.
  5. Build a minimal local slice with one meaningful boundary. Add packaged step
    definitions with breyta flows steps create/update/remove, add schedules
    with breyta flows schedules add/update/remove, and compose the
    orchestration separately with breyta flows compose; these helpers preserve
    the other top-level definition surfaces.
  6. Run fast feedback before mutating draft:
    • breyta flows lint --file ./flows/<slug>.clj --local-only for offline static checks: missing qualified packaged-step references, flow/step arity and shape errors that mirror push validation, and unreferenced packaged-step warnings (plain-literal forms only; reader-macro or runtime-computed forms are skipped by design)
    • breyta flows lint --file ./flows/<slug>.clj --server when API context is available and canonical pre-push checks matter; add --timeout <duration> for unusually slow server checks
    • breyta flows steps run <slug> <step-id> --params '<json>' to execute a packaged step from the complete local literal without persisting a draft
    • breyta flows push --file ./flows/<slug>.clj [--timeout 2m]; the timeout defaults to two minutes for each of the save and validation requests, and if either request times out, verify the draft with breyta flows show <slug> or breyta flows validate <slug> before retrying
    • breyta flows configure check <slug> when the flow has required slots
    • breyta flows validate <slug> after push for stored draft/live validation
  7. Prove the same entrypoint a user will exercise:
    • breyta flows run <slug> --interface-id <id> --input '<json>' --wait
    • omit --interface-id when the single manual interface can be inferred
    • keep at most one manual interface; use invocation inputs for mode choices
    • use --target live or --installation-id for release/install proof
    • inspect one step with breyta runs inspect <workflow-id> --step <step-id>
      before expanding the full run payload

Treat steps 5-7 as one loop per step, not one pass per flow: add one step,
run that step, read its real output, then write the next step; for
orchestration-only steps that steps run rejects, use a minimal flow run.
Composite debugging after five unproven steps hides which boundary lied.

Child Flow Targets

An author-owned child flow that declares required connections, secrets, or setup
values needs a profile target. During a draft run, an unqualified child call
automatically selects the enabled draft profile for the child flow in the same
workspace:

(let [input (flow/input)]
  (flow/call-flow :review-social-draft input))

The parent draft's inherited draft-version marker triggers this lookup; the
child's profile supplies the child flow's own bindings and setup values. The
source remains portable between workspaces and does not hardcode a profile id.
If the child has no enabled draft profile, pass an explicit child :profile-id
or configure the draft profile before running the parent.

For installed public children, use the installation id in the third options
map instead:

(flow/call-flow :reviewed-ai-response-generator
                {:prompt prompt}
                {:installation-id reviewed-ai-response-installation-id})

When the parent itself is executing inside a linked public installation, an
unqualified same-app child call inherits that linked context automatically.

Stop Discovery When

  • you have current state or a clear blank-workspace signal
  • one nearby workspace/template pattern is enough
  • docs for touched primitives are known
  • the next command is an edit, lint, push, configure check, run, or resource read

Do not repeat identical search/help/docs commands unless state or the question
changed.

If development needs excessive trial/error, docs/help are misleading, a command
shape is blocked or unclear, or examples do not cover the authoring path, submit
breyta feedback send with the commands tried, URLs/workflow ids, expected path,
and the friction. This helps Breyta evaluate whether the agent loop is working.

Complex Flow Phase Gates

Complex flows should still move in small slices. Start with this authoring
playbook, then phase-load only the next surface:

  • provider/model/API call: references/provider-api-freshness.md
  • paging, fanout, child flows, waits, checkpoints, or retries:
    playbooks/advanced-reliability.md
  • persisted reports, tables, files, media, or large outputs:
    references/outputs-and-tables.md and references/runtime-data-shapes.md
  • release, install, public, Discover, or marketplace work:
    the release/public playbooks

Before source grows, define a proof ladder and reuse it across steps run and
flows run:

  • skeleton interface run with one safe input
  • one-record or one-page local JSON fixture
  • representative small batch
  • empty/failure fixture
  • live or install-shaped input

For irreversible side effects such as email, CRM writes, billing, or external
mutations, build preview/dry-run behavior, an approval wait, or an explicit
author approval checkpoint before real writes. Add idempotency keys or dedupe
guards when retries or reruns can repeat a write.

Progressive Disclosure

Use search result cards first. Open the full template/source only for:

  • cross-step architecture reuse
  • install behavior
  • parent/child or fanout structure
  • unclear dependencies
  • public/marketplace patterns

Prefer primitive snippets and only copy referenced dependencies:
:requires, :templates, :functions, packaged :steps, and :agents.

Do not open a full template definition unless snippet context is insufficient or
architecture, install behavior, fanout, child-flow structure, or unclear
dependencies require it.

For step config questions, use field lookup before opening a full reference:

  • breyta docs fields http for a compact config-object overview
  • breyta docs fields http response-as persist retry --format json for selected
    fields
  • breyta docs fields files source paths --section read when a step page has
    operation-specific tables
  • breyta docs show reference-step-http --section "Canonical Shape" only when
    the field rows are not enough

Single-Step Development

Single-step runs are the required cadence for supported activity-backed
steps, not an optional optimization: build one step, run it, inspect the
real output, then write the next step. steps run rejects :sleep,
:wait, and :fanout, and control-plane steps only execute inside a flow
run, so prove orchestration-only steps with a minimal full-flow run instead.
Use single-step commands instead of full workflow reruns while developing
the touched primitive:

  • breyta steps run --type <type> --id <id> --params '<json>'
  • breyta steps run --flow <slug> --source draft --type <type> --id <id> --params '<json>'
  • breyta steps run --flow <slug> --source draft --type <type> --id <id> --params-file ./params.json
  • breyta steps record --flow <slug> --source draft --type <type> --id <id> --params '<json>'

Use --flow when the step depends on flow-local :templates, :functions,
:requires, packaged :steps, or installation bindings. Use --source draft
while iterating on a just-pushed draft; use --source active or --version <n>
when verifying released behavior. Add --profile-id <id> when slot-backed
connections should resolve from a saved draft/live profile; use
--installation-id <id> for installed-run proof. The command waits up to five
minutes by default; use --timeout 10m for a slower provider call.

For an LLM prompt stored in the selected flow's :templates, pass the template
ID and its variables separately. :template selects a flow-local template;
:data supplies the values used by {{...}} expressions:

breyta steps run --flow update-blog-post --source draft \
  --profile-id <profile-id> --type llm --id refresh-blog-post \
  --params-file ./tmp/refresh-blog-post.params.json --timeout 5m
{"template":"refresh-blog-post","connection":"ai","data":{"title":"Example","article":"..."}}

The --flow and source flags are what let the server resolve the flow-local
template. Replace ai with the connection slot declared by the flow. A
catalog/template slug is not a substitute for the flow's local template ID. If
the request still reaches the CLI deadline, increase --timeout; this changes
the client request bound, not the provider's own model or upstream timeout.

Use inline --params '<json>' for small inputs. Switch to --params-file when
the payload is long, reused, or easier to edit outside the command line. Build
the params from the smallest input map that exercises the touched primitive.
Good candidates are pure functions, HTTP/API request shape, LLM prompt shape,
file/resource read/write payloads, and notification payloads. Use care with real
side effects, waits, fanout, child flows, callbacks, and params mostly produced
by previous pipeline state.

steps run accepts JSON but normalizes safe step-config keys to the authored
Clojure shape. For example, responseAs becomes :response-as and
persist.type can become :persist {:type :blob}. HTTP payload surfaces such
as headers, query, body, json, and form keep their exact external key
shape. Normalization is recursive over nested maps and applies to packaged-step
input params too, so author kebab-case keys in every :input-schema and
cross-step contract; a camelCase schema key never matches its normalized
runtime key.

steps run returns compact output by default: data.resultPreview.value is an
EDN-style preview of the value as it would appear in a downstream let binding.
Use --result-path rows.0 or --result-path '[:rows 0]' to focus one branch,
--preview-depth, --preview-items, and --preview-runes for bounded
expansion, --result-file ./tmp/result.json to keep the full value locally, and
--full only when the complete data.result belongs in context.

Isolated step runs and stored step test verifies are real executions for
accounting: they authorize against workspace billing state, create an audit
workflowId, and count completed or failed step attempts toward step usage.

Single-step proof is not final proof. After the primitive works, push and run
the authored flow interface with the intended target, input, bindings, and side
effect path.

Interfaces First

New callable flows should use :interfaces and :invocations. --trigger-id
is legacy compatibility. For manual smoke runs, prefer --interface-id <id> so
the CLI exercise matches the authored user entrypoint.

Persist And Resource Refs

Default to :persist for unknown or unbounded payloads: exports, paged API
responses, files, transcripts, generated media, long Markdown, or row sets.

Choose blob tier deliberately: retained/default for durable or user-visible
artifacts; :tier :ephemeral on streaming :http steps for temporary downloads,
exports, generated media, and response blobs. Function/table/KV persists remain
retained/default today.

Persisted step results are compact refs. Downstream steps should pass refs
through maps and explicitly hydrate only where needed, for example a function
step with :input {:resp http-step-result} and :load [:resp].

Function Shape

Most runtime values are already Clojure maps. Keep :function steps small:

  • use get, get-in, select-keys, update, mapv, and destructuring
  • use json/parse (exactly one argument, no parser flags) and
    json/write-str for JSON
  • use breyta.sandbox/* helpers for common safe utilities
  • for time and dates, use allowlisted java.time.* interop plus
    breyta.sandbox instant helpers such as parse-instant and
    instant->epoch-ms; System/currentTimeMillis is blocked. The flow body
    has no Java interop; its deterministic time source is (flow/now-ms)
  • avoid large custom parser/guard layers unless external input is truly ambiguous
  • coalesce optional inputs with a first-present reduce over candidate keys,
    not or; only nil and false are falsey in Clojure, so or treats an
    explicit false as absent and silently disables opt-outs. Prove both cases
    with a step run: absent input takes the default, explicit false stays
    false
  • after any CLI/JSON round-trip, scan the source for doubled backslashes in
    regex literals; #"\\s+" matches a literal backslash plus s, not
    whitespace. Probe every decode and regex helper before shipping — packaged
    steps with breyta flows steps run, inline or :functions helpers with
    breyta steps run --flow <slug> --source draft --type function --id <id>;
    these bugs fail silently

Packaged Step Shape

Top-level :steps entries wrap one built-in step:

  • shape: {:id :ns/name :type <built-in> :description "..." :input-schema [:map ...] :defaults {...}} plus optional :output-schema,
    :prepare, :project-result, and :tool
  • :id must be a qualified keyword; :config is not a wrapper key, and
    :description plus :input-schema are required
  • the flow/step call's input map merges with :defaults into the wrapped
    step config; outer controls ride in that same input map
  • declare kebab-case :input-schema keys so they survive JSON param
    normalization
  • a step that fails under :on-error :continue binds nil; treat nil as
    the failure signal, and check response fields such as (:status result)
    only when the wrapped step's output contract declares them

Installable Mindset

Assume a new or edited flow may become installable later:

  • keep workspace-specific ids, user ids, secrets, emails, private URLs, and model
    choices out of source defaults
  • put installer/user-specific values in :requires, setup inputs, run inputs,
    connections, or resources
  • split child flows or packaged steps when isolated testing and reuse are clearer
    than one large orchestration
  • keep :flow focused on orchestration; reusable structure belongs in
    :requires, :templates, :functions, packaged :steps, and reusable
    :agents
  • grouping is mutable display metadata, not source. Verify groupFlows with
    breyta flows list --pretty or breyta flows show <slug> --pretty; runtime
    linkage still comes from flow/call-flow, :fanout, or another primitive

References

As of Jul 28, 2026