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 -> Artifactsopens a dedicated artifact sidepeek. - The canonical deep-link remains the run Output page.
Requestremains 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:
| Surface | Purpose |
|---|---|
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:
| Key | Meaning |
|---|---|
:breyta.viewer/kind | Viewer type to render (allowlisted). |
:breyta.viewer/value | Value payload for that viewer. |
:breyta.viewer/options | Optional 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:
| Field | Meaning |
|---|---|
:resource | Absolute 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. |
:title | Display 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, :name | Alternate display-name fields when :title is not present. |
:table | Table query and presentation options for :view :table. |
Resource Embed Pattern Reference
Use this table to choose the right embed shape:
| Pattern | Use when | Minimal directive |
|---|---|---|
:view :auto | Let Breyta choose from resource metadata/content type. | {:resource "...", :view :auto} |
:view :table with :query | Show a bounded filtered/sorted row snapshot. | {:resource "...", :view :table, :table {:query {...}}} |
:view :table with :aggregate | Show grouped metrics from a table resource. | {:resource "...", :view :table, :table {:aggregate {...}}} |
:view :table with :chart | Render a chart from the same query/aggregate rows. | {:resource "...", :view :table, :table {:aggregate {...} :chart {...}}} |
:view :download | Place a compact file/table download affordance in the report. | {:resource "...", :view :download} |
:view :markdown | Embed a persisted Markdown report/note. | {:resource "...", :view :markdown} |
:view :text | Embed a plain text resource. | {:resource "...", :view :text} |
:view :json | Embed persisted structured data as formatted raw output. | {:resource "...", :view :json} |
:view :image | Embed a persisted image blob. | {:resource "...", :view :image, :alt "..."} |
:view :audio | Embed a persisted audio blob. | {:resource "...", :view :audio} |
:view :video | Embed 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:
| Field | Meaning |
|---|---|
:select | Ordered fields to include in the snapshot. Omit only when the default table preview is acceptable. |
:where | Vector of filters, for example [["status" := "open"]]. Keep filters deterministic and bounded. |
:sort | Ordered sort clauses, for example [["created-at" :desc]]. |
:page | Required bounded page options. Use {:mode :offset :limit N} for v1 Markdown embeds. |
Column presentation fields:
| Field | Meaning |
|---|---|
:label | Human column heading. Prefer business labels over storage names. |
:format | Display formatting, for example currency or timestamp. |
:semantic-type / :type-hint | Optional 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:
| Field | Meaning |
|---|---|
:group-by | Fields to group rows by. |
:metrics | Metric definitions. Supported metric maps include :op, optional :field, and :as. |
:order-by | Ordered result sort clauses. |
:limit | Bounded 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:
| Field | Meaning |
|---|---|
:title | Optional chart title metadata. Full table resource panels may render it; Markdown embeds keep it out of the document chrome. |
:x | Label/category field. Defaults to the first visible field. |
:series | Vector of series maps. Each map supports :field, :label, and :type (:bar or :line). |
:height | Optional chart height in pixels. Defaults to 220 and is clamped to 120-480. |
:y-min, :y-max | Optional 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 Nfor aggregate table embeds when the default 25-row snapshot is not right - use
:row-transformonly for bounded final display shaping - keep resource URIs in the same workspace as the run
- keep each
breyta-resourcedirective 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-resourcefences 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:
| Need | Pattern |
|---|---|
| The structured value is the product output | Return {:breyta.viewer/kind :raw ...} with the map as :breyta.viewer/value. |
| The structured value is a persisted JSON resource inside a report | Use a Markdown breyta-resource fence with :view :json. |
| The value is supporting detail inside Markdown | Put it in a fenced code block with clojure, edn, or json. |
| The value should be a human summary | Convert 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:
| Note | Implication |
|---|---|
| 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:
| Need | Output shape |
|---|---|
| A short comparison or summary inside a report | Put a Markdown table inside a :markdown artifact. |
| A real grid for scanning, copying, exporting, or opening as a table resource | Persist 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/kindis:tablefor the table artifact.:breyta.viewer/valuehas:type :resource-ref.:breyta.viewer/valuehas: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.
| Do | Why |
|---|---|
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:
| Surface | Login | Intended use |
|---|---|---|
| Run Output route | Required | Workspace/user run inspection. |
breyta resources url / /api/resources/url | Usually auth to mint; temporary signed content URL | Direct resource access for operators and runtime handoff. |
/public/artifact-previews/:token | No login | Read-only, unlisted, noindex preview page for outreach or external review. |
/public/artifact-previews/:token/download | No login | Token-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/contentproxy URLs - no
res://refs,urialiases, 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-resourcefences and media/download URLs stay hidden instead of
being fetched through private resource routes - page responses include
Cache-Control: no-storeand 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:
- Create a preview share for a low-risk real output artifact.
- Open the returned
publicUrlin an incognito/no-login browser. - Confirm the safe human-facing content renders and the CTA points to the
expected relative claim path. - If the artifact is text-like, open
publicDownloadUrland confirm the
response is an attachment with the expected content and no private metadata. - Search the HTML for
res://,/api/resources/content,workspace=,
workflow-id,run-id,debug,trace,X-Goog, and storage host names.
None should appear. - 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)
| Viewer | Use |
|---|---|
:raw | Fallback for arbitrary structured output. |
:text | Plain text content. |
:markdown | Rich text rendering. |
:table | Tabular row output with curated human-readable columns. |
:image | Image URL/blob rendering. |
:audio | Audio URL/blob rendering. |
:video | Video URL/blob rendering. |
:download | Compact file/table download affordance. |
:group | Multi-part output in one envelope. |
JSON compatibility
If the final output is JSON (string keys), the UI also recognizes:
| JSON key | Meaning |
|---|---|
"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
| Guidance | Why |
|---|---|
| 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
- Start Here
- Flow Authoring
- Flow Definition
- Output Artifact Reference
- CLI Commands
- Runs And Outputs
- Persisted Results And Resource Refs
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:
- Call and persist the image API response — HTTP step with
:persist {:type :blob} - Load, decode, and persist the binary image — Function step with
:input {:resp resp}and:load [:resp] - 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:
| API | Base64 response path | Notes |
|---|---|---|
| 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). |