Docs
Reference

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

GoalUse
Call a public built-in step directly from orchestration, including :agent or :breytaflow/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 agentA packaged :steps wrapper with a narrow :input-schema
Reuse a heavy config surface with simpler input/outputA 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-steps or :tools {:allowed [...]}.
  • Direct :files tools 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-request in 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:

  • :files agent-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 :agent without
    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 :prepare or :project-result transforms that should
    run automatically when the step is invoked.

Definition Shape

Each packaged step definition is a map in the top-level :steps vector:

FieldTypeRequiredNotes
:idqualified keywordYesMust be qualified, e.g. :github/open-pr, :billing/fetch-order. Unqualified keywords are reserved for built-in step families.
:typekeywordYesThe 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.
:descriptionstringYesHuman-readable description. Used in validation errors, agent tool summaries, and the docs surface. Max 4000 chars.
:input-schemaschemaYesschema definition for the packaged step input. Push-time validation checks the schema shape and static literal invocation keys; runtime validates actual invocation values.
:output-schemaschemaNoschema definition for the projected output. Validated at runtime after :project-result.
:titlestringNoShort display title.
:defaultsmapNoDefault config merged into the wrapped step. Use for :connection, :template, :method, :path, etc.
:preparekeywordNoId of a top-level :functions entry. Called with the validated input; must return a map that becomes the wrapped step config.
:project-resultkeywordNoId of a top-level :functions entry. Called with the raw wrapped step result; must return the projected output.
:toolmapNoTool 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, :table are 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:

  1. Validates the input against :input-schema.
  2. Calls :prepare (if set) to transform input into wrapped step config.
  3. Merges :defaults under the prepared config.
  4. Validates the merged config against the wrapped built-in step schema.
  5. Executes the wrapped built-in step.
  6. Calls :project-result (if set) to transform the result.
  7. Validates the projected result against :output-schema (if set).
  8. 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-schema becomes the tool's parameter schema.
  • The step's :description (or :tool :description override) 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-only or :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 :steps entries match the PackagedStepDefinition schema.
  • Ids are qualified keywords with no duplicates.
  • :prepare and :project-result reference existing top-level :functions.
  • :input-schema and :output-schema are 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-schema before :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.
  • :prepare and :project-result must reference top-level :functions.
    Inline code is not supported.
  • Packaged steps are flow-local. There is no cross-flow or workspace-level
    step registry.
  • :defaults are shallow-merged. Deep merge of nested config maps is not
    supported.

Related

As of May 12, 2026