Docs
Operate

Output Artifacts (Final Output Viewers)

Quick Answer

Use this guide to shape flow final outputs for UI rendering using stable viewer envelopes, Markdown reports, and fenced breyta-resource embeds. For public/end-user and installed flow output, default to a single readable Markdown artifact.

For public/end-user flows, treat the final output as the product surface: return the human-readable artifact first, and keep raw automation/debug payloads out of the public result unless they are explicitly the thing the user asked for.

The default rich output pattern is a Markdown report. When that report needs
real Breyta resources, embed them in the Markdown with fenced
breyta-resource blocks so tables, charts, downloads, images, videos, nested
Markdown, and structured resources appear naturally in the report.

Flows always produce a final output: the value returned by the :flow form.

The Breyta UI shows this final output as a user-facing artifact (separate from debug inspection):

  • On the run page, Run data -> Artifacts opens a dedicated artifact sidepeek.
  • The canonical deep-link remains the run Output page.
  • Request remains a debug entrypoint and is intentionally separate from artifacts.

This document describes how flow authors can shape the final output for good presentation.

The flow definition does not currently declare a separate output schema. The
implicit contract is:

final value returned by :flow = run output

Breyta captures that raw value as the run's canonical flow-output resource,
stores :result-resource-uri on the execution, and derives a user-facing viewer
artifact from the same value. Author/debug surfaces may expose raw output,
previews, step outputs, and created resources, but the main Output view should be
treated as the user-facing artifact.

Where users see output

For each run, output can be accessed from two user-facing surfaces:

SurfacePurpose
Run page sidepeek (Artifacts)Primary, quick in-context output inspection.
Output route (/:workspace-id/runs/:flow-slug/:run-id/output)Canonical full-page output view and shareable deep-link.

When a run has no output yet:

  • Running/pending runs show Output not available yet.
  • Terminal runs show No output captured.

The viewer envelope (recommended)

Return an envelope map with these namespaced keys:

KeyMeaning
:breyta.viewer/kindViewer type to render (allowlisted).
:breyta.viewer/valueValue payload for that viewer.
:breyta.viewer/optionsOptional viewer config (title, alt text, etc.).

Example: Markdown report

{:breyta.viewer/kind :markdown
 :breyta.viewer/options {:title "Summary"}
 :breyta.viewer/value "# Report\n\nHello."}

Example: structured run metadata

{:breyta.viewer/kind :raw
 :breyta.viewer/options {:title "Run metadata"}
 :breyta.viewer/value {:report-id "daily-orders-2026-05-01"
                       :generated-for "Operations"
                       :open-orders 2
                       :closed-orders 1
                       :risk "low"
                       :next-review "2026-05-01T15:00:00Z"}}

Example: image (typically a Breyta-generated signed URL)

{:breyta.viewer/kind :image
 :breyta.viewer/options {:title "Screenshot" :alt "Screenshot"}
 :breyta.viewer/value "https://example.com/image.png"}

Example: audio/video (typically a Breyta-generated signed URL)

{:breyta.viewer/kind :audio
 :breyta.viewer/options {:title "Audio"}
 :breyta.viewer/value "https://example.com/audio.wav"}
{:breyta.viewer/kind :video
 :breyta.viewer/options {:title "Video"}
 :breyta.viewer/value "https://example.com/video.mp4"}

Markdown Resource Embeds

Markdown is the recommended default for narrative output. When the report needs
to include a real Breyta resource, return a Markdown viewer envelope and include
a fenced breyta-resource block.

Use normal Markdown table syntax when the rows are static explanatory content
inside the report. Use :view :table only when the report should render a
persisted Breyta table resource with query, aggregate, chart, download, or
drilldown behavior.

Resource embeds are snapshot-oriented. The output normalizer resolves the
referenced resource into snapshot metadata on the Markdown artifact, and the
Markdown renderer injects that snapshot where the fence appears. The end-user
output remains one continuous Markdown render: text, tables, images, videos, and
nested Markdown flow in document order instead of appearing as separate output
cards.

V1 supports snapshot embeds only. :mode :snapshot is the default, and
:mode :live is intentionally not supported yet. If an author sets a non-
snapshot mode, the renderer shows an inline unsupported-mode fallback instead of
live-querying the resource.

Use EDN or JSON inside the fenced block. EDN is convenient in flow definitions.
YAML directives are not supported in v1.

For a compact field-by-field lookup, use
Output Artifact Reference.

{:breyta.viewer/kind :markdown
 :breyta.viewer/options {:title "Order report"}
 :breyta.viewer/value
 (str "# Order report\n\n"
      "Filtered open orders:\n\n"
      "```breyta-resource\n"
      "{:resource \"" (:uri orders-table) "\"\n"
      " :view :table\n"
      " :title \"Filtered open orders\"\n"
      " :table {:query {:select [\"order-id\" \"customer\" \"amount\"]\n"
      "                 :where [[\"status\" := \"open\"]]\n"
      "                 :sort [[\"created-at\" :desc]]\n"
      "                 :page {:mode :offset :limit 25}}\n"
      "         :columns {\"amount\" {:label \"Amount\"\n"
      "                              :format {:display \"currency\" :currency \"USD\"}}}}}\n"
      "```\n")}

Supported fields:

FieldMeaning
:resourceAbsolute res://... URI in the current workspace.
:view:auto, :table, :markdown, :text, :json, :image, :audio, :video, or :download.
:mode:snapshot in v1. This is the default. :live is reserved and not supported.
:titleDisplay title or caption for the embedded resource, where the adapter uses one. For embedded tables, this is metadata only; put visible section headings in the surrounding Markdown.
:label, :nameAlternate display-name fields when :title is not present.
:tableTable query and presentation options for :view :table.

Resource Embed Pattern Reference

Use this table to choose the right embed shape:

PatternUse whenMinimal directive
:view :autoLet Breyta choose from resource metadata/content type.{:resource "...", :view :auto}
:view :table with :queryShow a bounded filtered/sorted row snapshot.{:resource "...", :view :table, :table {:query {...}}}
:view :table with :aggregateShow grouped metrics from a table resource.{:resource "...", :view :table, :table {:aggregate {...}}}
:view :table with :chartRender a chart from the same query/aggregate rows.{:resource "...", :view :table, :table {:aggregate {...} :chart {...}}}
:view :downloadPlace a compact file/table download affordance in the report.{:resource "...", :view :download}
:view :markdownEmbed a persisted Markdown report/note.{:resource "...", :view :markdown}
:view :textEmbed a plain text resource.{:resource "...", :view :text}
:view :jsonEmbed persisted structured data as formatted raw output.{:resource "...", :view :json}
:view :imageEmbed a persisted image blob.{:resource "...", :view :image, :alt "..."}
:view :audioEmbed a persisted audio blob.{:resource "...", :view :audio}
:view :videoEmbed a persisted video blob.{:resource "...", :view :video}

Table Query Embeds

Table query embeds support bounded query options and column presentation:

{:resource "res://v1/ws/ws-123/result/table/tbl_orders"
 :view :table
 :title "Filtered open orders"
 :table {:query {:select [:order-id :customer :amount]
                 :where [[:status := "open"]]
                 :sort [[:created-at :desc]]
                 :page {:mode :offset :limit 25}}
         :row-transform "(fn [row]\n  (assoc row :customer (str (:customer row) \" - review\")))"
         :columns {"amount" {:label "Amount"
                             :format {:display "currency"
                                      :currency "USD"}}}}}

Query fields:

FieldMeaning
:selectOrdered fields to include in the snapshot. Omit only when the default table preview is acceptable.
:whereVector of filters, for example [["status" := "open"]]. Keep filters deterministic and bounded.
:sortOrdered sort clauses, for example [["created-at" :desc]].
:pageRequired bounded page options. Use {:mode :offset :limit N} for v1 Markdown embeds.

Column presentation fields:

FieldMeaning
:labelHuman column heading. Prefer business labels over storage names.
:formatDisplay formatting, for example currency or timestamp.
:semantic-type / :type-hintOptional metadata when the table viewer can use it.

Common format examples:

{:columns {"amount" {:label "Amount"
                     :format {:display "currency" :currency "USD"}}
           "created-at" {:label "Created"
                         :format {:display "timestamp"}}
           "owner-email" {:label "Owner"
                          :format {:display "email"}}}}

For partitioned table families, pin the table/partition explicitly when the
snapshot needs to be deterministic:

{:resource "res://v1/ws/ws-123/result/table/tbl_orders_by_year"
 :view :table
 :title "2026 open orders"
 :table {:partition-key "2026"
         :query {:select [:order-id :status :amount]
                 :where [[:status := "open"]]
                 :page {:mode :offset :limit 25}}}}

Use :partition-key for one table, or the existing table target shapes
:partition-keys / :partitions when the table service query supports them.
If the resource is a family and no partition is specified, the renderer follows
the table service default; authors should specify the partition for stable final
output snapshots.

Table Aggregate Embeds

The same table resource can be embedded more than once with different views.
For aggregate snapshots, use :table {:aggregate ...} instead of
:table {:query ...}:

{:resource "res://v1/ws/ws-123/result/table/tbl_orders"
 :view :table
 :title "Orders by status"
 :table {:aggregate {:group-by [:status]
                     :metrics [{:op :count :as "orders"}
                               {:op :sum :field :amount :as :total-amount}]
                     :order-by [[:status :asc]]
                     :limit 10}
         :columns {"status" {:label "Status"}
                   "orders" {:label "Orders"}
                   "total-amount" {:label "Total"
                                   :format {:display "currency"
                                            :currency "USD"}}}}}

Aggregate fields:

FieldMeaning
:group-byFields to group rows by.
:metricsMetric definitions. Supported metric maps include :op, optional :field, and :as.
:order-byOrdered result sort clauses.
:limitBounded aggregate result size. Defaults to 25 rows and is capped at 100 rows.

Metric examples:

{:metrics [{:op :count :as "orders"}
           {:op :sum :field :amount :as :total-amount}
           {:op :avg :field :amount :as :average-order-value}]}

Chart Embeds From Tables

Table snapshots can also render a Breyta chart above the visible table rows.
The chart uses the same bounded query or aggregate result as the table, so the
rendered graph and grid stay in sync. In Markdown embeds, chart/table titles are
not rendered as extra chrome; put user-facing headings in the Markdown around
the resource block.

{:resource "res://v1/ws/ws-123/result/table/tbl_orders"
 :view :table
 :title "Orders by status"
 :table {:aggregate {:group-by [:status]
                     :metrics [{:op :count :as "orders"}]
                     :order-by [[:status :asc]]
                     :limit 10}
         :chart {:title "Order count by status"
                 :x "status"
                 :series [{:field "orders"
                           :label "Orders"
                           :type :bar}]}}}

Use :type :line for trend charts from the same table rows:

{:resource "res://v1/ws/ws-123/result/table/tbl_orders"
 :view :table
 :title "Order value trend"
 :table {:query {:select [:created-at :amount]
                 :sort [[:created-at :asc]]
                 :page {:mode :offset :limit 25}}
         :columns {"created-at" {:label "Created"
                                  :format {:display "timestamp"}}
                   "amount" {:label "Order value"
                             :format {:display "currency"
                                      :currency "USD"}}}
         :chart {:x "created-at"
                 :series [{:field "amount"
                           :label "Order value"
                           :type :line}]}}}

Chart options:

FieldMeaning
:titleOptional chart title metadata. Full table resource panels may render it; Markdown embeds keep it out of the document chrome.
:xLabel/category field. Defaults to the first visible field.
:seriesVector of series maps. Each map supports :field, :label, and :type (:bar or :line).
:heightOptional chart height in pixels. Defaults to 220 and is clamped to 120-480.
:y-min, :y-maxOptional y-domain overrides. Defaults include zero.

Chart x-axis and hover labels reuse the column display format for the :x
field. For date or timestamp period labels, set the matching column :format
instead of letting raw ISO strings become the chart labels. Charts include zero
in the y-domain by default; use explicit :y-min and :y-max only when a
custom scale is intentional.

Row Transform

Use :row-transform when the final table needs presentation logic that is more
specific than labels and built-in cell formats. The transform runs once while
the Markdown resource snapshot is produced, after the bounded query or aggregate
has resolved and before the run output is stored.

{:resource "res://v1/ws/ws-123/result/table/tbl_orders"
 :view :table
 :table {:query {:select [:order-id :customer :amount]
                 :where [[:status := "open"]]
                 :page {:mode :offset :limit 25}}
         :row-transform "(fn [row]\n  (assoc row :customer\n         (str (:customer row) \" - \"\n              (if (< (:amount row) 10)\n                \"batch pickup\"\n                \"monitor cutoff\"))))"}}

The function receives one row map and must return one row map. String field
names from table rows also receive safe keyword aliases for transform input, so
authors can usually use (:amount row) in EDN examples. Use this only for final
display shaping; durable data normalization belongs in the flow steps that
create the resource. Keep transform source small; oversized transform forms are
rejected before sandbox evaluation. For the available sandbox helpers, see the
breyta.sandbox and json helper reference.

Download Embeds

Rendered table embeds are document-native: they omit the table card header,
per-table copy actions, CSV controls, and pagination footers. Put source/export
affordances exactly where they belong in the report by adding a separate
:view :download resource fence. For table resources, :view :download
defaults to CSV.

{:resource "res://v1/ws/ws-123/result/table/tbl_orders"
 :view :download
 :title "Orders source CSV"}

Use :format :csv when you want to make the format explicit. Partitioned table
families can use the same top-level partition fields as table embeds:

{:resource "res://v1/ws/ws-123/result/table/tbl_orders_by_year"
 :view :download
 :title "2026 orders CSV"
 :partition-key "2026"
 :format :csv}

For non-table file/blob resources, :view :download renders a compact download
affordance using the resource content type:

{:resource "res://v1/ws/ws-123/result/blob/monthly-report.pdf"
 :view :download
 :title "Monthly report PDF"}

Markdown, Text, And JSON Embeds

Use readable resource embeds when a report should inline another persisted
artifact.

Persisted Markdown:

{:resource "res://v1/ws/ws-123/result/blob/analyst-note.md"
 :view :markdown
 :title "Analyst note"}

Plain text:

{:resource "res://v1/ws/ws-123/result/blob/dispatch-note.txt"
 :view :text
 :title "Dispatch note"}

JSON:

{:resource "res://v1/ws/ws-123/result/blob/run-metadata.json"
 :view :json
 :title "Run metadata"}

Use :view :json for persisted JSON resources inside Markdown. For final output
that is itself a map, return a :raw viewer envelope instead of trying to put
the map into Markdown prose.

Image, Audio, And Video Embeds

Media embeds use persisted Breyta blob resources and signed content URLs. They
do not require public bucket access.

Image:

{:resource "res://v1/ws/ws-123/file/blob/.../dashboard.svg"
 :view :image
 :title "Fulfillment lane snapshot"
 :alt "Fulfillment lane dashboard snapshot"}

Audio:

{:resource "res://v1/ws/ws-123/file/blob/.../call-summary.wav"
 :view :audio
 :title "Call summary audio"}

Video:

{:resource "res://v1/ws/ws-123/file/blob/.../packing-station.mp4"
 :view :video
 :title "Packing station clip"}

Auto Embeds

Use :view :auto when the resource content type is enough for Breyta to choose
the viewer. Prefer explicit views in public/end-user reports when the output
contract matters.

{:resource "res://v1/ws/ws-123/result/blob/analyst-note.md"
 :view :auto
 :title "Analyst note"}

The final Markdown output exposes one floating top-right copy action for the
whole report instead of one copy action per embedded table. The visible renderer
uses the resolved Breyta resource snapshots. The copied Markdown is a portable
export: table snapshots become Markdown tables, Markdown resources become normal
Markdown, text resources become fenced text code blocks, and JSON/raw
resources become pretty-printed fenced clojure code blocks.

Resource-backed links in copied Markdown intentionally point back into Breyta
when a resource URI is known. Image embeds are copied as a Markdown image whose
source is an absolute Breyta content URL, wrapped in a link to the Breyta
resource viewer. Audio, video, and download embeds are copied as normal Markdown
links to the Breyta resource viewer. Embeds that cannot be converted remain in
the original source form.

Keep embeds bounded:

  • include :page {:mode :offset :limit N} for table embeds
  • include :limit N for aggregate table embeds when the default 25-row snapshot is not right
  • use :row-transform only for bounded final display shaping
  • keep resource URIs in the same workspace as the run
  • keep each breyta-resource directive small and use the documented values for
    :view, :mode, and :format
  • use :mode :snapshot; live resource-backed pagination is a future contract
  • expect the final run output to render as a single Markdown document; the raw
    breyta-resource fences are an authoring/source shape, not the user-visible
    output

Structured Maps And JSON

Do not paste raw EDN/JSON maps into normal Markdown paragraphs. Paragraph text
is prose, so structured values can collapse into a single hard-to-read line.

Use one of these patterns instead:

NeedPattern
The structured value is the product outputReturn {:breyta.viewer/kind :raw ...} with the map as :breyta.viewer/value.
The structured value is a persisted JSON resource inside a reportUse a Markdown breyta-resource fence with :view :json.
The value is supporting detail inside MarkdownPut it in a fenced code block with clojure, edn, or json.
The value should be a human summaryConvert it into prose, bullets, or a Markdown table before returning it.

Markdown code-block example:

```clojure
{:report-id "daily-orders-2026-05-01"
 :generated-for "Operations"
 :open-orders 2
 :closed-orders 1
 :risk "low"
 :next-review "2026-05-01T15:00:00Z"}
```

JSON resource embed example:

```breyta-resource
{:resource "res://v1/ws/ws-123/result/blob/run-metadata.json"
 :view :json
 :title "Run metadata"}
```

Breyta-managed media (no public URL required)

Breyta media artifacts are usually stored as managed blobs. The browser still needs a fetchable src, but it can be a time-limited URL generated by Breyta instead of a public URL.

Recommended pattern: persist the media as a blob and return the persisted blob result (Breyta will mint/refresh a signed URL when rendering the run Output page):

(let [download (flow/step :http :download-video
                          {:connection :api
                           :path "/video.mp4"
                           :method :get
                           :persist {:type :blob
                                     :tier :ephemeral
                                     :content-type "video/mp4"}})]
  {:breyta.viewer/kind :video
   :breyta.viewer/options {:title "Video"}
   :breyta.viewer/value download})

Tip: you can also return the persisted blob result directly and let the UI infer the viewer (it uses :content-type and fetches a signed URL as needed):

(let [download (flow/step :http :download-audio
                          {:connection :api
                           :path "/audio.wav"
                           :persist {:type :blob
                                     :tier :ephemeral
                                     :content-type "audio/wav"}})]
  download)

For HTTP-downloaded media, prefer :tier :ephemeral on the :http step unless the artifact is intentionally durable and should live like a retained file beyond the immediate workflow.

Persisted blob outputs carry both resource and storage shapes: :uri / :resource-uri are the canonical resource refs for UI/API handoff, while [:blob-ref :path] is the concrete blob storage path used by loaders and storage-backed steps.

Notes:

NoteImplication
Signed URLs expire.UI refreshes signed URLs at render time.
Need long-lived download links.Generate fresh signed URL via new run or dedicated download flow.

Multi-part output (group, recommended)

Use :group when you want to return multiple artifacts in one run. In v1 this is the recommended pattern for multi-artifact output:

{:breyta.viewer/kind :group
 :breyta.viewer/items
 [{:breyta.viewer/kind :markdown
   :breyta.viewer/options {:title "Summary"}
   :breyta.viewer/value "# Hello"}

  {:breyta.viewer/kind :image
   :breyta.viewer/options {:title "Image"}
   :breyta.viewer/value "https://example.com/image.png"}

  {:breyta.viewer/kind :raw
   :breyta.viewer/options {:title "Raw"}
   :breyta.viewer/value {:ok true :data [1 2 3]}}]}

Human-Readable Tables

Choose the table surface based on what the user needs to do with the result:

NeedOutput shape
A short comparison or summary inside a reportPut a Markdown table inside a :markdown artifact.
A real grid for scanning, copying, exporting, or opening as a table resourcePersist rows with :persist {:type :table ...} and return the persisted table resource ref from a :table viewer.

For a real Breyta table, return the persisted table step result. Inline maps with :rows, :columns, :schema, or :query are preview data, not table resources.

(let [run-id (str "run-" (flow/now-ms))
      comparison-table
      (flow/step :function :build-comparison-table
                 {:input {:rows comparison-rows
                          :run-id run-id}
                  :code '(fn [{:keys [rows run-id]}]
                           {:rows
                            (mapv (fn [row]
                                    {:run_id run-id
                                     :paragraph (:paragraph row)
                                     :original (:original row)
                                     :cleaned (:cleaned row)
                                     :changed (:changed row)})
                                  rows)})
                  :persist {:type :table
                            :table (str "transcript-comparison-" run-id)
                            :rows-path [:rows]
                            :write-mode :upsert
                            :key-fields [:run_id :paragraph]
                            :columns [{:column :paragraph
                                       :display-name "Paragraph"}
                                      {:column :original
                                       :display-name "Original"}
                                      {:column :cleaned
                                       :display-name "Cleaned"}
                                      {:column :changed
                                       :display-name "Changed"}]}})]
  {:breyta.viewer/kind :table
   :breyta.viewer/options {:title "Original vs cleaned"}
   :breyta.viewer/value comparison-table})

For multi-part output, put the table item first only when the grid is the primary artifact. Otherwise put a Markdown report first and the persisted table second:

{:breyta.viewer/kind :group
 :breyta.viewer/options {:title "Transcript QA"}
 :breyta.viewer/items [{:breyta.viewer/kind :markdown
                        :breyta.viewer/options {:title "Summary"}
                        :breyta.viewer/value report-markdown}
                       {:breyta.viewer/kind :table
                        :breyta.viewer/options {:title "Paragraph comparison"}
                        :breyta.viewer/value comparison-table}]}

Verify real table output before release:

  • :breyta.viewer/kind is :table for the table artifact.
  • :breyta.viewer/value has :type :resource-ref.
  • :breyta.viewer/value has :content-type "application/vnd.breyta.table+json".
  • [:breyta.viewer/value :preview :rows-written] or [:breyta.viewer/value :preview :row-count] is greater than zero.
  • breyta resources read <table-uri> returns rows.

Public flow output contract

Public and installable flows need a presentation contract, not just an automation payload. Before release, decide what a non-author should see after a manual run and return that shape directly.

DoWhy
Put the primary human artifact first in :breyta.viewer/items.The first artifact is the strongest signal for run Output and artifact surfaces.
Default to one readable :markdown artifact.Markdown can include headings, summaries, links, images, small tables, and video/file links without exposing raw data.
Use :table only when users need a real grid for scanning, comparing, copying, or exporting rows.Tables are useful for datasets, but they should not be the default public output.
Use :image, :video, or :audio only when a dedicated media viewer is clearly better.Dedicated media viewers are best for Breyta-managed blobs and signed URL handling.
Use human column labels such as Creator, Followers, Contact, and Profile URL.Public output should not expose database/debug fields.
Persist internal tables/blobs for automation, then keep their refs out of the final public result unless users need them.Resource refs and internal tables can be promoted or inspected in ways that expose implementation details.
Use :raw only when the intended public output is structured data.Raw output is a debug/automation surface by default.

Avoid mixing public presentation and internal automation output:

;; Bad for public manual runs: exposes internal resources and raw debug data.
{:breyta.viewer/kind :group
 :breyta.viewer/items [{:breyta.viewer/kind :markdown
                        :breyta.viewer/options {:title "Report"}
                        :breyta.viewer/value report-markdown}
                       {:breyta.viewer/kind :raw
                        :breyta.viewer/options {:title "Structured output"}
                        :breyta.viewer/value output-data}]
 :lead-table-ref (:uri lead-table)
 :candidate-table-ref (:uri candidate-table)
 :run-table-ref (:uri runs-table)}

Prefer a curated public result with markdown first:

;; Good for public manual runs: readable markdown first, optional table second.
{:breyta.viewer/kind :group
 :breyta.viewer/options {:title "Influencer research results"}
 :breyta.viewer/items [{:breyta.viewer/kind :markdown
                        :breyta.viewer/options {:title "Human-readable report"}
                        :breyta.viewer/value report-markdown}
                       {:breyta.viewer/kind :table
                        :breyta.viewer/options {:title "Outreach-ready leads"}
                        :breyta.viewer/value lead-table}]
 :status run-status
 :summary human-summary}

For table outputs, persist only user-facing rows and labels.

  • Good public labels: Who, Followers, Contact, Link, Why relevant
  • Avoid: debug previews, table ids, resource refs, status counters
  • If the table is short or explanatory, keep it inside Markdown

Before release, run live or installation target and open Output. Confirm the first artifact is readable, and no raw viewer, internal ref, or implementation-only field is visible.

No-login public artifact previews

Use public artifact preview links when a generated artifact needs to be shared
with someone outside the workspace, such as creator outreach, prospect review,
or a claim/edit onboarding path.

Public artifact preview links are different from run Output links and signed
resource URLs:

SurfaceLoginIntended use
Run Output routeRequiredWorkspace/user run inspection.
breyta resources url / /api/resources/urlUsually auth to mint; temporary signed content URLDirect resource access for operators and runtime handoff.
/public/artifact-previews/:tokenNo loginRead-only, unlisted, noindex preview page for outreach or external review.
/public/artifact-previews/:token/downloadNo loginToken-scoped download for text-like shared artifacts such as EDN, Markdown, CSV, JSON, XML, JavaScript, form data, and plain text.

Create and revoke preview links through the authenticated resource-share API:

POST /api/resources/shares
DELETE /api/resources/shares/:token
GET /public/artifact-previews/:token
GET /public/artifact-previews/:token/download

Send X-Breyta-Workspace: <workspace-id> on the authenticated create and
revoke API calls.

Create request body:

{
  "uri": "res://v1/ws/<workspace-id>/result/...",
  "title": "Creator preview",
  "ttlSeconds": 3600,
  "claimUrl": "/signup?source=creator-preview",
  "allowDownload": true
}

ttlSeconds is optional. The default is 30 days, the minimum is 60 seconds,
and the maximum is 90 days. claimUrl is optional and must be a same-origin
relative path; unsafe absolute or protocol-relative URLs are ignored.
allowDownload is optional and defaults to false. Enable it only when the
resource itself is intended to be delivered as a file to the recipient.

Public previews render the artifact in a content-only mode:

  • no workspace, run, workflow, debug, trace, or raw resource metadata
  • no private /api/resources/content proxy URLs
  • no res:// refs, uri aliases, private resource-content URLs, or common
    signed storage URLs in rendered artifact fields
  • no resource drilldown, metadata popovers, Open, Export, or Download actions
  • public previews render the already-created artifact; unresolved
    breyta-resource fences and media/download URLs stay hidden instead of
    being fetched through private resource routes
  • page responses include Cache-Control: no-store and noindex robot headers

When allowDownload is true and the artifact is text-like, the create response
also returns publicDownloadUrl. The download route uses the same unlisted
token, expiry, and revoke state as the preview page. It serves a bounded
attachment with Cache-Control: no-store and without exposing the private
/api/resources/content route or signed storage URLs. Binary media and
sandboxed HTML previews are not downloadable through this public route.

Production smoke check:

  1. Create a preview share for a low-risk real output artifact.
  2. Open the returned publicUrl in an incognito/no-login browser.
  3. Confirm the safe human-facing content renders and the CTA points to the
    expected relative claim path.
  4. If the artifact is text-like, open publicDownloadUrl and confirm the
    response is an attachment with the expected content and no private metadata.
  5. Search the HTML for res://, /api/resources/content, workspace=,
    workflow-id, run-id, debug, trace, X-Goog, and storage host names.
    None should appear.
  6. Revoke the share and confirm the preview and download URLs stop working.

Inference (optional, no envelope)

If you don’t use an envelope, the UI may infer a media viewer from common shapes:

{:url "https://example.com/file.png" :content-type "image/png"}
{:signed-url "https://example.com/file.wav" :content-type "audio/wav"}

If inference doesn’t match what you want, wrap the value in an explicit envelope.

Supported viewers (currently)

ViewerUse
:rawFallback for arbitrary structured output.
:textPlain text content.
:markdownRich text rendering.
:tableTabular row output with curated human-readable columns.
:imageImage URL/blob rendering.
:audioAudio URL/blob rendering.
:videoVideo URL/blob rendering.
:downloadCompact file/table download affordance.
:groupMulti-part output in one envelope.

JSON compatibility

If the final output is JSON (string keys), the UI also recognizes:

JSON keyMeaning
"breyta.viewer/kind"Viewer type.
"breyta.viewer/value"Viewer payload value.
"breyta.viewer/options"Optional viewer config.
"breyta.viewer/items"Multi-part items for :group.

Guidance

GuidanceWhy
Prefer explicit envelopes for end-user-facing flows.Produces predictable rendering behavior.
Keep outputs reasonably sized.UI truncates large raw outputs by default.
Prefer URLs/resource refs for media over huge inline strings.Better performance and reliability; Data URIs suit only small demos.

Related

Generating images from AI APIs (base64 responses)

Many image APIs return base64 strings instead of URLs. Do not pass base64 directly as :breyta.viewer/value; persist binary bytes first.

When responses may exceed 512 KB, persist the HTTP step first. Then hydrate with :load before decoding base64.

The working pattern is three steps:

  1. Call and persist the image API response — HTTP step with :persist {:type :blob}
  2. Load, decode, and persist the binary image — Function step with :input {:resp resp} and :load [:resp]
  3. Return a viewer envelope — Use the persisted image blob as the viewer value
(let [;; Step 1: Call and persist the image API response
      resp (flow/step :http :generate-image
                      {:connection :image-api
                       :method     :post
                       :path       "/images/generations"
                       :timeout    120
                       :json       {"model" "gpt-image-1.5"
                                    "prompt" prompt
                                    "size" "1024x1024"
                                    "quality" "low"
                                    "output_format" "jpeg"}
                       :persist    {:type :blob
                                    :tier :ephemeral
                                    :filename "image-response.json"}})

      ;; Step 2: Load the persisted HTTP response, decode base64 → binary bytes, persist as blob
      ;; breyta.sandbox/base64-decode-bytes must be called with full namespace
      img  (flow/step :function :save-image
                      {:input   {:resp resp}
                       :load    [:resp]
                       :persist {:type :blob
                                 :filename "image.jpeg"
                                 :content-type "image/jpeg"}
                       :code    '(fn [{:keys [resp]}]
                                    (-> resp
                                        :body
                                        :data
                                        first
                                        :b64_json
                                        breyta.sandbox/base64-decode-bytes))})]

  ;; Step 3: Return the persisted image blob as the viewer value
  {:breyta.viewer/kind    :image
   :breyta.viewer/options {:title "Generated image"}
   :breyta.viewer/value   img})

Use :tier :ephemeral on the HTTP response persist when that response is just a temporary handoff. The derived image blob persisted from the function step uses the retained default today.

For multiple images, use :group:

{:breyta.viewer/kind  :group
 :breyta.viewer/items [{:breyta.viewer/kind    :image
                        :breyta.viewer/value   landscape-img
                        :breyta.viewer/options {:title "Landscape"}}
                       {:breyta.viewer/kind    :image
                        :breyta.viewer/value   portrait-img
                        :breyta.viewer/options {:title "Portrait"}}]}

Why not return the base64 string directly as the blob?

Storing a base64 string with :content-type "image/jpeg" creates a text blob. The UI will not render it as an image. The breyta.sandbox/base64-decode-bytes step converts it to actual binary bytes first.

Inline result size limit

Base64 image responses often exceed the 512 KB inline limit. Persist first when size is uncertain, then hydrate with :load.

See also: breyta.sandbox helpers in Step Function reference

API response formats — common gotchas

Different image APIs return data differently. Make sure you're reading from the right path:

APIBase64 response pathNotes
OpenAI gpt-image-1 / gpt-image-1.5(-> resp :body :data first :b64_json)Always base64. output_format must be "jpeg", "png", or "webp" — not "b64_json" (DALL-E syntax).
OpenAI DALL-E 3(-> resp :body :data first :b64_json)Only when response_format: "b64_json". Default returns a URL — skip the decode step.
Google Imagen (Vertex AI)(-> resp :body :predictions first :bytesBase64Encoded)Content type in (-> resp :body :predictions first :mimeType).
As of May 20, 2026