Outputs And Tables Reference
Load this reference before creating or editing final outputs, public artifacts,
persisted resources, table viewers, or run evidence.
Output Defaults
- Public/end-user flow outputs should default to one readable Markdown artifact, or a deliberate table/media viewer when that is better for the user.
- The default rich output pattern is a Markdown report. Use fenced
breyta-resourceblocks when that report needs real Breyta tables, charts, downloads, images, video, nested Markdown, text, or JSON resources. - Keep raw debug maps, internal ids,
res://refs, and implementation-only fields out of final public output unless the user explicitly asked for them. - Include enough summary context that a user can understand the result without reading step internals.
- Include exact failure or skip reasons when that is the user-facing outcome.
- Do not paste EDN/JSON maps into Markdown prose. Return them as
:rawviewer values, embed persisted JSON with:view :json, or put supporting values in fenced code blocks.
Artifact Audience Review
After a run returns output or artifacts, inspect the actual user-facing result
before calling the workflow done. Review it from the perspective of the flow's
intended user and the audience of the artifact, not from the perspective of the
flow author.
Use the right inspection path:
- Markdown/report: open or read the rendered output and check whether the first screen explains the result, the content answers the user's request, links/resources resolve, and there are no raw debug maps or placeholders.
- Table: read the table resource, verify it has rows and useful columns, and judge whether the table is scannable for the intended user.
- Image: open the image artifact/resource and use vision analysis to check whether the visual content is legible, correctly framed, complete, and aligned with the requested audience/use.
- Video: if browser tooling is available, capture screenshots from several moments and review them. Otherwise use the best available frame extraction path and report the limitation.
- Download/blob: inspect metadata and, when safe and useful, read or preview the file enough to confirm it is the expected artifact.
Audience review should catch empty outputs, cut-off text, broken embeds,
illegible images, video frames that never show the intended content, excessive
debug detail, wrong tone, missing context, and outputs that satisfy the data
contract but fail the end-user experience.
Output Shape Decision Table
| User need | Preferred output shape |
|---|---|
| Narrative summary or report | {:breyta.viewer/kind :markdown ...} |
| Short explanatory table inside text | Markdown table inside a Markdown viewer |
| Durable grid for scan/copy/export | Persist rows as a table resource, then return :table or embed it with breyta-resource |
| Filtered or aggregated table inside a report | Markdown viewer with breyta-resource :view :table and bounded query/aggregate options |
| Chart from table rows | Markdown viewer with table :chart options in the resource fence |
| Source/export affordance | Separate breyta-resource fence with :view :download |
| Image/audio/video | Persist a blob and return or embed :image, :audio, or :video |
| Structured map or JSON as the product | :raw viewer, not Markdown prose |
| Supporting structured detail in Markdown | Fenced code block with clojure, edn, or json |
Markdown resource embeds are authoring syntax. The end-user output should render
as one continuous document; users should not see the raw fenced blocks or
technical res:// URIs.
The floating Copy Markdown action exports resolved embeds, not Breyta-only
fences, whenever possible. Tables become Markdown tables, JSON/raw resources
become readable fenced Clojure blocks, download/video/audio resources become
links to the Breyta resource viewer, and image embeds keep an image preview
wrapped in a Breyta resource-viewer link.
For field-by-field syntax, use the product docs:
breyta docs find "output artifact reference breyta-resource table aggregate".
Full Breyta URLs
Report full Breyta URLs whenever the CLI returns them:
- flow URL
- run URL
- run output URL
- activation/setup URL
- resource URL
- public template URL
- Discover/install URL
Prefer CLI-returned URLs:
data.flow.webUrldata.run.webUrlrun.webUrloutputWebUrldata.webUrlmeta.webUrlerror.actions[].url
Hand-build URLs only when the CLI does not return one and all required ids are
known.
Markdown Table Or Breyta Table Resource
When the user asks for "table output", decide first which output type they
mean.
Use a markdown table when:
- the table is short
- the table is explanatory
- the table belongs inside a readable report
- copy/export/filter behavior is not important
Use a Breyta table resource when:
- there are many rows
- the user needs scan/copy/export behavior
- the result should be durable structured data
- another step or human should consume the table as a resource
When the table belongs in a larger written report, embed the persisted table in
Markdown with a breyta-resource fence instead of returning a separate table
viewer as the whole output. Use a separate :view :download fence when the user
should get a CSV/source download.
Canonical Table Ownership
Treat persistent workspace tables as shared data products, not flow-local
scratchpads. Before creating one:
- Inventory related table resources and flows that read or write tables. Use
indexed listing only as a convenience scan, then verify the result against
the canonical resource store. - Read representative rows from plausible candidates.
- Trace each candidate's grain, key, schema, writers, scheduled rebuilds, and
consumers. Verify whether its writers preserve fields they do not own. - Classify the table as canonical current state, source/campaign data,
derived output, or append-only audit history. - Designate one system of record for each durable entity grain that multiple
flows must coordinate around.
Use bounded page sizes for discovery:
# Fast indexed candidate scan. This is not authoritative on its own.
breyta resources list --workspace <workspace-id> --accept application/vnd.breyta.table+json --limit 100
# Authoritative resource-store scan. Keep result resources whose contentType is
# application/vnd.breyta.table+json.
breyta resources list --workspace <workspace-id> --type result --limit 100
# Catalog/list cross-check. Repeat with --cursor while meta.hasMore is true,
# but do not treat this projection-backed listing as authoritative on its own.
breyta flows list --workspace <workspace-id> --include-archived --limit 100 --page-size 100
breyta flows list --workspace <workspace-id> --include-archived --cursor <nextCursor> --limit 100 --page-size 100
# Direct source reads for every returned flowSlug and every configured target.
breyta flows show <flow-slug> --workspace <workspace-id> --target live --full
breyta flows show <flow-slug> --workspace <workspace-id> --target draft --full
# User-visible installed-app candidate scan. Authenticate as a service account
# with installations.read for a workspace-wide inventory.
breyta flows installations list --all --workspace <workspace-id>
breyta flows installations show <profileId> --workspace <workspace-id>
# Exact source read for a local installation only: sourceWorkspaceId is absent
# or equals the consumer workspace. Linked-app full source is provider IP and is
# not available through the installer CLI.
breyta flows show <flowSlug> --workspace <workspace-id> --version <version> --full
# Optional index-backed candidate scans. These are not authoritative on their
# own.
breyta flows grep ":type :table" --or "flow/step :table" --workspace <workspace-id> --scope workspace --target live --limit 100 --from 0
breyta flows grep ":type :table" --or "flow/step :table" --workspace <workspace-id> --scope workspace --target draft --limit 100 --from 0
breyta flows grep "<table-name-or-key>" --workspace <workspace-id> --scope workspace --target live --limit 100 --from 0
breyta flows grep "<table-name-or-key>" --workspace <workspace-id> --scope workspace --target draft --limit 100 --from 0
breyta resources read <table-uri>
Do not treat the indexed --accept listing or the first page as complete. For
the authoritative resources list --type result scan, continue with
--cursor <next-cursor> while has-more is true and retain every item whose
contentType is application/vnd.breyta.table+json. Do not add --accept or
--query to this verification pass because those select the search-backed
picker path.
Treat flows list as a catalog-backed cross-check, not an authoritative
flow-store enumeration. Continue with --cursor <nextCursor> while
meta.hasMore is true, require the cursor to advance, and collect every
returned flowSlug. For every returned slug, run flows show --full
separately with --target live and --target draft, then inspect
data.flowLiteral for table reads, writes, and persisted table references. A
Draft not found response or the CLI's draft target is not configured error
records the draft target as absent. A no_active_version code,
Flow has no active version response, or the CLI's
live target is not configured error records the live target as absent. Any
other unreadable target or listing error makes the cross-check incomplete.
Process one flow/target at a time so large definitions do not accumulate in
memory. Archived flows are not active writers, but include them to recover
prior ownership and migration contracts.
Before authorizing a new table, require either an operator/admin-provided direct
flow-store enumeration or proof that the flow catalog has been reconciled
against the flow store for this inventory. If neither is available, report the
flow inventory as incomplete and do not create a new table. Exhausting
flows list alone cannot prove that every authored flow was returned.
Also inventory installations for the consumer workspace. A normal user's
flows installations list --all result is filtered to that user and is not a
workspace-wide inventory. Run it as a service-account principal with
installations.read, use an operator/admin direct profile-store inventory, or
prove that every workspace member's installation inventory has been included.
If none is available, report the installation inventory as incomplete and do
not create a new table.
The workspace installation endpoint is paginated to 100 items and returns
data.nextCursor with data.hasMore. The current CLI command exposes the first
page but no installation cursor flag. If data.hasMore is true, use an
operator/admin API or store path that accepts data.nextCursor, require the
cursor to advance, and continue until data.hasMore is false. Do not repeat the
same first-page CLI command or call that result complete.
For every returned item, retain its profileId, flowSlug,
sourceWorkspaceId, sourceFlowSlug, owner when available, enabled state, and
exact installed version; inspect details with
flows installations show <profileId>. When sourceWorkspaceId is absent or
equals the consumer workspace, read the local exact version with
flows show <flowSlug> --version <version> --full.
When sourceWorkspaceId differs, the installation is linked. Do not run the
consumer-workspace flows show command: it can select an unrelated same-named
local flow, and the installer's supported read is intentionally a redacted
summary rather than the provider's full source literal. Account for the linked
item as an opaque potential writer with its exact pinned metadata. For any
table grain that may overlap the app's purpose or observed resources, require a
source-authorized operator/provider table-ownership contract for that version;
otherwise fail closed for that grain. An opaque linked app does not make an
unrelated grain incomplete when the non-overlap is explicitly documented.
Enabled installations are potential current writers; disabled installations
still carry prior ownership and migration history.
Use flows grep only to accelerate candidate review, never to choose the only
flows whose sources are inspected. Run every grep query separately with
--target live and --target draft; the default latest target can hide an
active live writer when an unpublished draft exists. Before using any grep
page, inspect timedOut in both the result and metadata. It must not be true;
an absent field is the normal non-timeout response. Discard a timed-out partial
page and retry with a narrower pattern or surface. On a non-timed-out page,
continue with --from <offset>, advance by the returned size, and stop when
the page returns fewer results than the requested limit.
Only consider the inventory complete after the authoritative resource-store
scan has has-more false; the direct flow-store inventory or catalog
reconciliation proof is complete; every returned authored slug's configured
live/draft sources have been read; every installation inventory page and owner
has been accounted for; every local installed source has been read; and every
linked installation has either a resolved ownership contract for the relevant
grain or a documented non-overlap. Exhausted flows list and grep pages are
useful cross-checks, not proof of flow-store completeness.
Use this ownership model:
| Table role | Policy |
|---|---|
| Canonical entity/current state | Keep one logical shared table or partitioned family per entity grain. Upsert by an immutable internal entity id. |
| Source or campaign data | May remain separate, but reference the canonical entity id and do not make global decisions independently. |
| Event or audit ledger | Keep immutable history separately, ingest idempotently by a stable source event id, and reconcile relevant state into the canonical entity row. |
| Replacement table | Use only as a bounded migration. Name the old and new authority and define coexistence and retirement. |
Extend or migrate the canonical table by default. Do not create a new table
merely because the flow, campaign, or source is new. A separate table is
justified when the data has a genuinely different grain or lifecycle, requires
different security or retention, or is part of a documented migration. Do not
reuse a table based only on its name: an incompatible key or a scheduled writer
that rebuilds a narrow schema can silently lose fields and recreate duplicates.
CRM Pattern
- Keep one canonical contact table or partitioned table family shared by all
lead research, outreach, reply, and monitoring flows in the workspace. - Generate an immutable internal
contact_idonce. Treat LinkedIn ids, emails,
and profile URLs as mutable aliases, never as the canonical row key. Resolve
aliases through an explicit matching and merge process; preserve the
surviving internal id when an alias changes or duplicate people are merged. - Put alias matching, canonical-id allocation, and alias attachment behind one
enforceable identity-resolution boundary before a contact becomes eligible
to send. Either start one dedicated resolver flow through a concurrency-aware
top-level HTTP/interface/event or API invocation with
:concurrency {:type :singleton :on-new-version :drain}and no direct
contact-id creation elsewhere, or use a transactional external store with a
uniqueness constraint for each provider/account/alias-type/normalized-value
key. Never invoke the resolver withflow/call-flowor another child-flow
path: child execution does not enter the top-level active-lane/drain
coordinator. Prove top-level behavior withconcurrencyDecision, run timing,
and the run list. Inside that boundary, normalize and re-read every supplied
alias, reuse or merge the winning contact, and allocate a newcontact_id
only when none exists. A serialized table-backed resolver must verify all
deterministic alias-index writes before marking the contact eligible. On a
uniqueness conflict, partial write, rejection, timeout, or unknown result,
re-read and reconcile; never return both candidate contacts as send-eligible. - Plan capacity before a concrete table reaches
50_000live rows. Keep one
logical CRM by using first-class:partitioningwith a deterministic bounded
bucket such ashash(contact_id) mod N, whereNis at most128. Store the
bucket in a field such ascontact_partition, use a:fieldpartition
strategy, and compute the same bucket for every point read or write. Use an
external database instead when identity matching or eligibility requires
broad cross-partition scans. - An ordinary canonical-table upsert is not a lock. Never use
resolve/upsert -> eligibility check -> claim across independent sending flows
because two runs can both pass the check. - Put every first-touch side effect behind one enforceable serialization
boundary. Either start one dedicated sender flow through a concurrency-aware
top-level HTTP/interface/event or API invocation with keyed concurrency on the
raw immutablecontact_idand:on-new-version :drain, with no other flow
allowed to send directly, or use a transactional external store/provider
that conditionally creates a unique policy-scoped send key such as
first-touch:<contact_id>. Never useflow/call-flowor another child-flow
path to enter this sender lane. Prove the top-level start's
concurrencyDecisionandconcurrencyKey. Inside the serialized sender lane,
re-read eligibility and persist the claim before the side effect. If neither
boundary is available, fail closed and do not automate the send. Before
selecting the keyed sender lane, resolve current aliases again to the
surviving canonical id; an alias disagreement, superseded id, or pending
merge remains ineligible. - Record pending, sent, failed, and ambiguous outcomes against the canonical
contact. A timeout or unknown post-send state remains ineligible until
reconciled; do not retry it as a fresh first touch. - Keep campaign qualification and social-post evidence in campaign/source
tables when useful, but reference the canonical contact id. - Keep send, acceptance, and reply events as audit ledgers. Ingest them
idempotently with a stable provider event or delivery id, scoped by provider
and account when needed, and upsert or deduplicate on that key while keeping
the accepted event row immutable. A workflow id, run id, or workflow-derived
table operation id does not deduplicate a provider redelivery that starts a
new run. These ledgers inform the canonical row; they are not competing CRMs. - Never create another CRM table for a new outreach flow when the canonical
contact table family can be extended, partitioned, or migrated.
If a new persistent table is still required, report the candidates rejected
and why, the new table's grain and key, its owner and consumers, whether it is
canonical or supporting data, and its migration or retirement plan.
Uploaded File To Downloadable Artifact Checklist
Use this uploaded-file-to-downloadable-artifact checklist when the user asks to
upload a document/file and receive a downloadable result:
- use
:filefor a new browser or CLI upload, and:resourcewhen the user can
pick an existing workspace resource - declare an invocation input with
:type :filefor a new upload, or
:type :resourcewhen the user can pick an existing workspace resource - set
:acceptto the expected MIME types - treat uploaded values as resource refs, not inline bytes
- call installed/public apps by outcome first when OCR or another common
substep is needed - persist generated Markdown, text, JSON, DOCX, PDF, or binary output with
:persist {:type :blob ...} - use
:loadonly where a downstream function needs hydrated persisted content - return a Markdown viewer with a
breyta-resource:view :downloadfence - smoke test with
breyta flows run --upload, then inspect
breyta resources workflow list <workflow-id>and
breyta resources read <uri>
Full recipe:
breyta docs show guide-persisted-results-and-resources --section "Uploaded File To Downloadable Artifact Recipe".
Breyta Table Resource Verification
For a Breyta table resource, verify all of these before calling the workflow
done:
- final output has
:breyta.viewer/kind :table - viewer value has
:type :resource-ref - resource content type is
application/vnd.breyta.table+json preview.rows-written > 0breyta resources read <table-uri>returns rows
Do not consider breyta resources workflow list alone sufficient verification.
It proves a resource exists, not that the user-facing table resource is readable
and populated.
Table Write And Read Shapes
For :persist {:type :table ...} writes, use the table-name form in the
:table field. Table queries and reads can use table resource refs, but a
result table URI in a persist write can pass draft validation and still fail at
live runtime parameter validation.
After writing rows, read the table back with breyta resources read <table-uri>
or the table step/query path that downstream consumers will use. Confirm the
returned URI/ref is non-empty and that rows are present before storing or
reporting the table as ready.
If a flow stores a table URI/ref in KV or a registry, never overwrite a known
good value with nil or an empty value from a later upsert/write result. Keep
the previous ref or fail loudly so downstream reads do not inherit a corrupted
registry entry.
Large Artifact Hygiene
Agents should inspect large artifacts through refs and bounded previews, not by
pasting full tables or reports into chat. Use breyta resources read <uri> for
the default compact blob preview or default bounded table preview and add
--full only when the full payload is required for the current decision.
Treat --pretty as formatting only; it must not imply full payload access.
When a flow produces a long Markdown report, transcript, JSON body, or research
artifact, persist the body as a blob/resource and pass the resource ref, signed
URL, and a short summary through run results or table rows. Keep durable tables
scan-friendly: store status, counts, source refs, output refs, and short previews
instead of giant cells like report_markdown that contain the whole artifact.
For blob resources, choose the tier up front: retained/default for durable or
user-visible artifacts; :tier :ephemeral for temporary streamed HTTP blobs,
downloads, exports, generated media, and intermediate API responses. Function,
table, and KV persists use the retained/default path today.
Persist And Resource-Ref Handoff
Default to :persist when payload size is unknown or unbounded: provider
exports, paged API responses, generated media, long Markdown reports,
transcripts, JSON snapshots, files, and tables.
Persisting changes the shape passed to later steps:
- persisted blobs carry resource refs in
:uri/:resource-uri - persisted blobs also carry storage loader details under
:blob-ref - persisted tables should be treated as canonical
{:type :resource-ref :uri ...}maps - UI/API/report handoff should use the resource URI or canonical resource-ref
- downstream blob hydration should pass the whole persisted step result and use
:loadon the function step
Use this handoff shape:
(let [resp (flow/step :http :fetch-export
{:connection :api
:path "/export"
:accept :json
:persist {:type :blob
:tier :ephemeral
:content-type "application/json"}})
rows (flow/step :function :extract-rows
{:input {:resp resp}
:load [:resp]
:code '(fn [{:keys [resp]}]
(get-in resp [:body :items]))})
table (flow/step :function :persist-rows
{:input {:rows rows}
:code '(fn [{:keys [rows]}] {:rows rows})
:persist {:type :table
:table "export-rows"
:rows-path [:rows]
:write-mode :upsert
:key-fields [:id]}})]
{:breyta.viewer/kind :table
:breyta.viewer/value table})
Use (:resource-uri result) or (:uri result) when a command, table row, or
Markdown breyta-resource fence needs a URI string. Keep the full persisted
result when a downstream step can load it. Do not use [:blob-ref :path] as a
public/user-facing ref; it is a storage pointer for loaders and storage-backed
steps.
Persisted Resources
Use breyta resources ... to inspect persisted result refs like res://....
breyta resources listlists refs by filters.breyta resources workflow list <workflow-id>lists refs for a run.breyta resources get <uri>fetches metadata.breyta resources read <uri>reads persisted results.breyta resources url <uri>gives a shareable URL.
For large outputs, pass refs through the flow rather than large bodies. Keep
inline final output small and user-facing.
Runtime Proof
Runtime proof should include:
- exact command run
- workflow/run id
- terminal status
- user-facing output summary
- artifact audience review notes
- required side-effect evidence
- full Breyta URLs
- unresolved risk or blocked verification
Do not report success based only on flows push or resources workflow list.
Behavior is proven by a representative run, by reading the outputs/resources
that matter, and by reviewing the actual artifact as the intended user would.
For notification flows, include the rendered or submitted payload in the proof:
recipient, sender, subject, relevant body copy, and any required absence checks
such as "no PR link" or "no debug map".