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:requireslist where two slots
share a provider account and origin - public unauthenticated endpoints need no connection slot;
:httpwith a
plain:urlworks for public GETs such as sitemaps and feeds - declare one connection per provider account and origin; prefer a
bare-origin:base-urlwith 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.mdbefore 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
- Start a small session capsule:
- workspace, flow slug, task mode
- intended interface and target
- refs consulted
- chosen pattern
- next proof command
- risks
- Reuse before writing:
breyta flows search "<integration or pattern>" --limit 5breyta flows grep "<literal>" --surface definition,tools --limit 5when metadata is not enoughbreyta flows templates search "<problem>" --limit 5breyta flows templates grep "<step or config>" --surface steps,tools --limit 5when template metadata is not enoughbreyta resources search "<existing data>" --limit 5when prior uploads, reports, or run outputs may be reused- treat hits like
rg: carry forwardhitRef, matched field/surface,
snippet/preview, andnextCommand; 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
- Define the contract before source grows:
:requires:invocations:interfaces:concurrency- output shape
- side effects and failure behavior
- For a new flow, run
breyta flows init <slug> --name "...". It creates the
local canonical.cljsource with one no-input manualruninterface and
an empty:schedulesvector. For an existing flow, usebreyta flows pull. - Build a minimal local slice with one meaningful boundary. Add packaged step
definitions withbreyta flows steps create/update/remove, add schedules
withbreyta flows schedules add/update/remove, and compose the
orchestration separately withbreyta flows compose; these helpers preserve
the other top-level definition surfaces. - Run fast feedback before mutating draft:
breyta flows lint --file ./flows/<slug>.clj --local-onlyfor offline static checks: missing qualified packaged-step references,flow/steparity 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 --serverwhen API context is available and canonical pre-push checks matter; add--timeout <duration>for unusually slow server checksbreyta flows steps run <slug> <step-id> --params '<json>'to execute a packaged step from the complete local literal without persisting a draftbreyta 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 withbreyta flows show <slug>orbreyta flows validate <slug>before retryingbreyta flows configure check <slug>when the flow has required slotsbreyta flows validate <slug>after push for stored draft/live validation
- Prove the same entrypoint a user will exercise:
breyta flows run <slug> --interface-id <id> --input '<json>' --wait- omit
--interface-idwhen the single manual interface can be inferred - keep at most one manual interface; use invocation inputs for mode choices
- use
--target liveor--installation-idfor 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.mdandreferences/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 httpfor a compact config-object overviewbreyta docs fields http response-as persist retry --format jsonfor selected
fieldsbreyta docs fields files source paths --section readwhen a step page has
operation-specific tablesbreyta 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.jsonbreyta 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-strfor JSON - use
breyta.sandbox/*helpers for common safe utilities - for time and dates, use allowlisted
java.time.*interop plus
breyta.sandboxinstant helpers such asparse-instantand
instant->epoch-ms;System/currentTimeMillisis 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,
notor; onlynilandfalseare falsey in Clojure, soortreats an
explicitfalseas absent and silently disables opt-outs. Prove both cases
with a step run: absent input takes the default, explicitfalsestays
false - after any CLI/JSON round-trip, scan the source for doubled backslashes in
regex literals;#"\\s+"matches a literal backslash pluss, not
whitespace. Probe every decode and regex helper before shipping — packaged
steps withbreyta flows steps run, inline or:functionshelpers 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 :idmust be a qualified keyword;:configis not a wrapper key, and
:descriptionplus:input-schemaare required- the
flow/stepcall's input map merges with:defaultsinto the wrapped
step config; outer controls ride in that same input map - declare kebab-case
:input-schemakeys so they survive JSON param
normalization - a step that fails under
:on-error :continuebindsnil; treatnilas
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
:flowfocused on orchestration; reusable structure belongs in
:requires,:templates,:functions, packaged:steps, and reusable
:agents - grouping is mutable display metadata, not source. Verify
groupFlowswith
breyta flows list --prettyorbreyta flows show <slug> --pretty; runtime
linkage still comes fromflow/call-flow,:fanout, or another primitive