Packaged Steps (:steps)
Quick Answer
Use top-level :steps when you want to wrap a heavy built-in step config
behind a simplified input/output contract that can be invoked directly from
flow/step and published as an agent tool from :agent / :llm.
Packaged steps are flow-local definitions — they live in the flow EDN alongside
:functions, :templates, and :flow. They do not create new step families;
they wrap one existing built-in step.
For the full list of public step families you can call directly with
flow/step, see Step Reference. Public direct
step families include :agent and :breyta; packaged steps are an additional
wrapper pattern for supported built-in families when you need a simplified,
schema-backed contract or agent tool publication.
Direct Steps Vs Packaged Wrappers
| Goal | Use |
|---|---|
Call a public built-in step directly from orchestration, including :agent or :breyta | flow/step with the built-in step family |
| Expose agent-safe built-in tools directly | :available-steps [:files :table :search], string tool names in :tools {:allowed [...]}, or allowlisted :tools {:breyta ...} |
| Expose broad raw operations safely to an agent | A packaged :steps wrapper with a narrow :input-schema |
| Reuse a heavy config surface with simpler input/output | A packaged :steps wrapper |
Do not read this page as the complete step-family list. It explains when to
wrap a step config. The complete public list lives in
Step Reference, with separate pages for
Step Agent and Step Breyta.
When To Use Packaged Steps
Required for agent tool use with these step types:
:http,:db,:kv,:function,:notify,:sleep,:wait,:ssh- These have broad raw parameter surfaces and cannot be exposed directly as
agent tools via:available-stepsor:tools {:allowed [...]}. - Direct
:filestools are limited to the agent-safe operations
:init-changeset,:list,:read,:search,:write-file,
:apply-edit,:replace,:replace-lines,:delete-file,:move-file,
and:diff. Wrap:resolve-source,:capture,:publish, and
:open-change-requestin packaged steps when an agent needs those
capabilities. - If you try, you'll get a clear error: "cannot be exposed directly as an
agent tool. Wrap it in a packaged :steps definition."
Not required (but still useful) for agent-safe step types:
:filesagent-safe operations,:table,:search- These have narrow, agent-oriented parameter schemas and can be exposed
directly.
Also useful when:
- The underlying built-in step has a large config surface (connections,
templates, complex request shaping) and you want a smaller invocation
contract for flow authoring. - You want to publish a simplified tool interface to
:agentwithout
exposing the full built-in step config to the model. - You want input/output schema validation at the packaged boundary, separate
from the wrapped step's own validation. - You have reusable
:prepareor:project-resulttransforms that should
run automatically when the step is invoked.
Definition Shape
Each packaged step definition is a map in the top-level :steps vector:
| Field | Type | Required | Notes |
|---|---|---|---|
:id | qualified keyword | Yes | Must be qualified, e.g. :github/open-pr, :billing/fetch-order. Unqualified keywords are reserved for built-in step families. |
:type | keyword | Yes | The supported wrapped built-in step type, such as :http, :files, :llm, :table, or :db. See Step Reference for all public direct step families; not every direct step family should be packaged. |
:description | string | Yes | Human-readable description. Used in validation errors, agent tool summaries, and the docs surface. Max 4000 chars. |
:input-schema | schema | Yes | schema definition for the packaged step input. Push-time validation checks the schema shape and static literal invocation keys; runtime validates actual invocation values. |
:output-schema | schema | No | schema definition for the projected output. Validated at runtime after :project-result. |
:title | string | No | Short display title. |
:defaults | map | No | Default config merged into the wrapped step. Use for :connection, :template, :method, :path, etc. |
:prepare | keyword | No | Id of a top-level :functions entry. Called with the validated input; must return a map that becomes the wrapped step config. |
:project-result | keyword | No | Id of a top-level :functions entry. Called with the raw wrapped step result; must return the projected output. |
:tool | map | No | Tool publication metadata for :agent / :llm. Contains optional :name and :description overrides. |
Id Rules
- Packaged step ids must be qualified keywords:
:namespace/name. - Examples:
:github/open-pr,:billing/fetch-order,:slack/post-message. - Unqualified keywords like
:http,:files,:tableare reserved for
built-in step families and will fail validation. - Ids must be unique within a flow. Duplicate ids fail at push time.
Canonical Example
{:functions [{:id :prepare-open-pr
:language :clojure
:code "(fn [{:keys [owner repo title head base]}]
{:method :post
:path (str \"/repos/\" owner \"/\" repo \"/pulls\")
:json {:title title
:head head
:base base
:draft true}
:response-as :json})"}
{:id :project-pr-result
:language :clojure
:code "(fn [result]
{:url (get-in result [:body :html_url])
:number (get-in result [:body :number])
:state (get-in result [:body :state])})"}]
: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]
[:number :int]
[:state :string]]
:defaults {:connection :github-api}
:prepare :prepare-open-pr
:project-result :project-pr-result
:tool {:name "github_open_pr"
:description "Open a draft GitHub pull request."}}]
:flow
'(let [pr (flow/step :github/open-pr :create-pr
{:owner "acme"
:repo "service"
:title "Fix null handling"
:head "task/fix-null"
:base "main"
:persist {:type :blob}})]
{:pr-url (:url pr)
:pr-number (:number pr)})}
Direct Invocation
Packaged steps are invoked through flow/step using the packaged step id
instead of a built-in step type:
(flow/step :github/open-pr :step-id
{:owner "acme"
:repo "service"
:title "Fix: handle null case"
:head "task/fix-null"
:base "main"})
The runtime:
- Validates the input against
:input-schema. - Calls
:prepare(if set) to transform input into wrapped step config. - Merges
:defaultsunder the prepared config. - Validates the merged config against the wrapped built-in step schema.
- Executes the wrapped built-in step.
- Calls
:project-result(if set) to transform the result. - Validates the projected result against
:output-schema(if set). - Returns the projected result.
Outer Step Controls
Common step controls like :retry, :timeout, :persist, :on-error,
:breakpoint, :mock, :expect, :review, and :confirm belong on the
outer flow/step call, not inside the packaged step definition:
(flow/step :github/open-pr :create-pr
{:owner "acme"
:repo "service"
:title "Fix it"
:head "task/fix"
:base "main"
:retry {:max-attempts 3}
:persist {:type :blob}
:on-error {:strategy :continue}})
These controls are separated from the packaged input before validation and
reattached around the wrapped execution.
Agent Tool Publication
Packaged steps can be published as agent tools through the :tools config
on :agent or :llm steps:
(flow/step :agent :review
{:connection :ai
:objective "Review and fix the issue."
:tools {:steps [:github/open-pr :billing/fetch-order]
:allowed ["files" "table"]}
:max-iterations 8})
When :tools {:steps [...]} lists packaged step ids:
- The step's
:input-schemabecomes the tool's parameter schema. - The step's
:description(or:tool :descriptionoverride) becomes the
tool description. - The step's
:tool :name(or a sanitized version of the id) becomes the
tool name. - Tool calls from the model are validated against the packaged input schema,
then executed through the same prepare/execute/project pipeline. - The packaged step's
:defaults,:connection,:template, and helper
functions are resolved at runtime — the model only needs to provide the
simplified input.
Use string tool names in :tools {:allowed [...]}, for example
["files" "table"]. If you want built-in step keywords instead, use
:available-steps [:files :table].
This means a packaged step works identically whether invoked directly from
flow/step or called as a tool by an agent.
Connection-Scoped Tool Permissions
For packaged steps that wrap connection-backed built-ins such as :http, you
can add flow-local runtime guardrails on the corresponding :requires slot.
These permissions are authored on the requirement, not on the workspace
connection record itself, so the same connection can be used with different
scopes in different flows.
Example:
{: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"]}}}}]
:steps [{:id :github/list-pulls
:type :http
:description "List repository pull requests."
:input-schema [:map]
:defaults {:connection :github-api
:path "/repos/breyta/breyta/pulls"}}]}
Supported generic HTTP permission hints are:
:mode— high-level access mode such as:read-onlyor:read-write:allowed-methods— explicit allowlist of HTTP verbs:allowed-hosts— host allowlist:allowed-path-prefixes— allowed path prefixes under the resolved base URL:allowed-url-prefixes— allowed absolute URL prefixes
These guards are enforced at packaged-step runtime, including when the packaged
step is published as an agent tool. The model still only sees the packaged
step's simplified :input-schema, but runtime also verifies that the wrapped
step stays within the authored connection scope.
Prepare and Project-Result Functions
:prepare and :project-result reference top-level :functions by id.
:prepare transforms validated packaged input into the config map for
the wrapped built-in step:
;; Input: {:owner "acme" :repo "service" :title "Fix" :head "task/fix" :base "main"}
;; Output: {:method :post :path "/repos/acme/service/pulls" :json {...} :response-as :json}
If :prepare is omitted, the validated input is passed directly as the
wrapped step config.
:project-result transforms the raw wrapped step result into the
packaged output:
;; Input: {:status 201 :body {:html_url "..." :number 42 ...}}
;; Output: {:url "..." :number 42 :state "open"}
If :project-result is omitted, the raw wrapped step result is returned.
Both functions execute in a secure sandbox with the same constraints as
top-level :functions.
Schema Validation
Push time: The flow definition validator checks that:
- All
:stepsentries match thePackagedStepDefinitionschema. - Ids are qualified keywords with no duplicates.
:prepareand:project-resultreference existing top-level:functions.:input-schemaand:output-schemaare valid schema definitions.- Direct packaged-step invocations with literal map input only use the schema to
catch unknown keys and missing required keys. Dynamic values and literal value
types are validated at runtime because they may depend on workflow execution.
Runtime: On each invocation:
- Input is validated against
:input-schemabefore:prepare. - The prepared config is validated against the wrapped built-in step schema.
- Output is validated against
:output-schema(if present) after:project-result.
Local Source Includes
Large packaged step definitions can live in sidecar files using the
#flow/include form:
{:steps [#flow/include "flow-assets/steps/github-open-pr.edn"
#flow/include "flow-assets/steps/billing-fetch-order.edn"]}
Includes are expanded client-side by breyta flows push before upload.
Installation Notes
Packaged steps often reference connections in :defaults:
{:steps [{:id :github/open-pr
:type :http
:defaults {:connection :github-api}
...}]}
The :connection :github-api reference is resolved through the flow's
:requires bindings at runtime. In installed flows, the installer provides
the connection during setup (or the author provides it via
:provided-by :author).
Key rule: Packaged step :defaults should reference connection slots,
not hardcoded connection IDs. This way the binding resolves correctly in both
the author's workspace and any installation.
;; Good: references a slot that resolves through :requires
:defaults {:connection :github-api}
;; Bad: hardcoded to the author's specific connection
:defaults {:connection "pwtucmXckXmJGspGF4i2"}
When a packaged step is published as an agent tool via :tools {:steps [...]},
the same connection binding applies — the agent's tool call executes with the
installation's bound connection, not a hardcoded one.
Current Limitations
- Packaged steps wrap one underlying built-in step. Multi-step and
subflow-backed packaged steps are not supported. :prepareand:project-resultmust reference top-level:functions.
Inline code is not supported.- Packaged steps are flow-local. There is no cross-flow or workspace-level
step registry. :defaultsare shallow-merged. Deep merge of nested config maps is not
supported.
Related
- Flow Definition — top-level
:stepsfield - Step Agent — agent tool publication via
:tools {:steps [...]} - Installations — installable agent flows
- Step Reference — built-in step families
- Step Function — top-level
:functionsused by:prepare/:project-result