Skip to main content
Plexara
Product11 min read

603 - Outputs: feeds, reports, and dashboards that refresh themselves

What a run produces and where it lands. One script and one output name is one asset, and every run adds a version; a dated name builds an archive instead. Rows become CSV or JSON feeds; a string body becomes a markdown, HTML, or JSX document. A semi-dynamic dashboard is published once and has only its data region refreshed on later runs, so layout edits made in the portal survive. A dashboard can instead reference a file a script rewrites, capped versions keep an asset tidy, and a named bucket drop delivers the same bytes to another system.

What you will take away from this lesson

A script that only prints is a log. What makes a managed script worth running on a schedule is what it leaves behind: a feed another system reads, a report somebody opens, a dashboard whose numbers move while its layout stays put. This lesson is about those outputs, where they land, and what identity they keep from one run to the next.

The example is the script from 602, which grew from two feeds to a dashboard across three saved versions on the demo tenant. Two of its runs were refused on the way, and both refusals are worth reading, because each one is a rule about outputs stated in the platform’s own words.

Learning Objectives

  1. 01Choose an output shape: rows serialized as a CSV or JSON feed, or a document body written as markdown, text, HTML, or JSX.
  2. 02Understand output identity: one script and one output name is one asset, and every run adds a version; a dated name builds an archive instead.
  3. 03Build a semi-dynamic dashboard: publish the document once, refresh only its data region on later runs, and keep the layout edits made in the portal.
  4. 04Use the referencing pattern when a document should show a file’s current content: the script refreshes the file, and every document naming it shows the new content without being re-saved.
  5. 05Cap the versions an asset a script rewrites keeps, and keep a registered table current from a script.
  6. 06Deliver the same bytes to a bucket drop by naming a destination and a key, and read what the run records about it.

Where this lesson sits

The 602 script exists, has been dry-run, and has been saved. This lesson is about the other end of it: the feeds, documents, and dashboards a run writes, and the identity each one keeps from one run to the next. It uses the same script, followed through three more saved versions.

600 Series: Automations

Open index

The 600 series picks up the registered table from the 500 series and the prompt mechanics from the 400 series. It is the last numbered series in the curriculum.

What a run produces, and where it is recorded

A run records what it did: its status and timing, the queries it issued, the outputs it wrote, and the log the script printed. The outputs are the part that matters to everyone who is not the script’s author. Each one is named in the script, and each one lands as a portal asset with a version number, linked from the run’s page.

The ledger below is the real record of three runs of the 602 script on the demo tenant. The middle run failed partway, for a reason the next sections explain, and its record still names what it had written before it stopped.

Three runs of one script, and what each left behind

  • dpx_88dfb063…

    v3, trigger tool, 8,542 ms

    succeeded

    supplier-margin-by-category → asset 871da875… version 1 (csv, 50 rows, 3,404 bytes); supplier-margin-feed → asset 589855b4… version 1 (json, 1 row, 27,660 bytes)

  • dpx_ba4f9dd0…

    v5, mode=publish

    failed at line 174

    supplier-margin-by-category version 2; supplier-margin-feed version 2; supplier-margin-dashboard → asset bab7c8e7… version 1 (html, document, 4,562 bytes). The outputs that landed before the failure stayed.

  • dpx_3bea162a…

    v6, mode=refresh, 8,231 ms

    succeeded

    supplier-margin-by-category version 3; supplier-margin-feed version 3; supplier-margin-dashboard version 2, refresh: true, 24,020 bytes spliced into the island

Every run records its outputs by name, with the asset each one landed in and the version it wrote. The same three names appear on every run because the names are the identity: three runs did not create nine assets, they wrote three versions of three assets. The middle run failed, and its record still names what it wrote before it stopped.

The run’s page

Every run has an address of its own in the portal. The page shows the parameters it ran with, how long it took, each output with a link to the asset version it wrote, and the log. A scheduled run and a run asked for by hand have the same page.

A single run’s page in the portal: the parameters it ran with, its timing, the outputs it wrote with each one linking to the asset version it produced, and the log the script printed.
A run’s own page. Every output it wrote is listed by name and links to the asset version it produced; an output delivered to a drop names where it was written instead. The log underneath is what the script printed.

Identity across runs

The most consequential decision about an output is its name, because the name is what persists. Two shapes cover every case, and the choice between them is made when the script is written, not later.

The output name is the identity

  • A stable name

    name = "supplier-margin-by-category"

    What happens across runs
    One asset. Every run writes a new version of it, so the asset keeps its id, its shares, and its history. On the demo, three runs took it from version 1 to version 3.
    When to use it
    A feed a dashboard references, a report people bookmark, a dashboard on a wall. Anything that should have one address.
  • A dated name

    name = "daily-sales-" + report_date

    What happens across runs
    One asset per date. Each run creates a new one, and the old ones stay exactly as they were written.
    When to use it
    An archive series, where each run is its own kept document and nobody wants yesterday’s numbers to move.

The pair of script and output name maps to one asset. A different script writing the same name gets its own asset, so two scripts never overwrite each other’s work by accident. The dated example is the one the dialect reference ships with: the date comes from the run’s pinned fire time, never from a clock.

Tables and documents

An output carries its content in one of two shapes: a list of rows, which the platform serializes in the declared format, or a string body, which lands byte for byte. Which shapes a format accepts is a rule, not a preference, so a feed another system parses stays well-formed by construction and a document arrives exactly as the script composed it.

Six formats, two shapes

csv
A list of dicts, one per row
A CSV asset another system can parse, well-formed by construction
json
A list of dicts
A JSON asset: a feed a dashboard references or a system reads
html
A string body, written verbatim
An HTML document: a dashboard or a formatted report
jsx
A string body
A JSX document, rendered by the portal like any saved JSX asset
markdown
Either rows or a body
A markdown page: a prose report, or a table from rows
text
Either rows or a body
A plain-text document

The declared format decides which shape is valid, and a mismatch fails the run rather than guessing. The 602 script hit this on its second dry run, when the JSON feed was passed as a single dict: “rows must be a list of dicts, or a string body for a document format (html, jsx, markdown, text), got dict.” Wrapping the feed in a list was the whole fix. An empty document body is refused the same way, so a conditionally assembled report that ends up blank fails loudly instead of replacing the current version of a shared dashboard with nothing.

A dashboard that refreshes itself

A document can be produced two ways, and the choice is made before its first line is written. Compose the whole document in the script when each run is its own kept document, when the structure has to vary with the data, or when nobody will ever edit the presentation; the cost is that every fire overwrites the current version wholesale, so a heading fixed in the portal is destroyed by the next run.

Publish the document once and refresh only its data region when there is one stable-named dashboard at one address whose layout a person may edit and whose numbers alone move per run. That is the semi-dynamic dashboard, and it is what the 602 script became in versions 4 through 6.

The data region, and the two lines that publish and refresh it

In the document

<script type="application/json" id="data">{}</script>
<script>
  const data = JSON.parse(document.getElementById("data").textContent);
  // render KPIs, bars, and the at-risk table from data
</script>

In the script, version 6

if run.params["mode"] == "publish":
    platform.export(
        name = "supplier-margin-dashboard",
        rows = DASHBOARD,
        format = "html",
    )
else:
    platform.publish_data("supplier-margin-dashboard", feed)

The document marks exactly one element with id="data", and its own code reads that element at view time. platform.publish_data serializes the payload as JSON and replaces the interior of that one element, leaving every other byte of the document as its author wrote it. The document has to exist first, as an HTML, JSX, or markdown output of the same script under the same name, which is what the publish mode does once. A document without the region fails the run rather than being written anywhere else.

Publishing it, and the rule the first run broke

The first run of the dashboard version did two things under one output name in one run, and the platform refused the second. The refusal is a rule about outputs worth knowing before it costs you a run of your own.

Publishing the dashboard, as it happened on the demo tenant

What the agent calls. This is the exchange the agent has with Plexara on your behalf, shown for the technical reader. You ask in plain language; you never type any of it.

  1. 1

    run_script name=acme-supplier-margin args={month: 2025-12-01, mode: publish} (version 5: export the document, then publish_data, in one run)

    failed at line 174. Error in platform.publish_data: output "supplier-margin-dashboard" was already written to "portal" by this run; each output name may be written once per destination, so give the second one its own name. Outputs that landed: supplier-margin-dashboard → asset bab7c8e7… version 1 (html, document, 4,562 bytes).

  2. 2

    manage_script command=patch name=acme-supplier-margin (publish_data only when mode is refresh)

    Saved, and this version is what runs now. Version 6.

  3. 3

    run_script name=acme-supplier-margin args={month: 2025-12-01, mode: refresh}

    succeeded, 8,231 ms. supplier-margin-dashboard → asset bab7c8e7… version 2, refresh: true, 24,020 bytes.

The first run wrote the document and then tried to refresh it in the same run, and the platform refused: one output name may be written once per destination in a run. The document it had already written stayed as version 1. The fix was a branch, and the next run wrote version 2 with only the data region changed. From here on every scheduled fire is a refresh, and every version is an as-of snapshot: an old version still shows exactly the numbers it showed, and a public share works with no live query behind it. A layout edit made in the portal survives, because the script never touches the markup again. If a run finds no rows, that is the script’s decision: publish the empty structure, or fail("why").

What the dashboard looks like

The dashboard the script publishes carries the same layout as the one built by hand in 504: the coverage figures, the cost change by category, and the at-risk table. The difference is that nobody rebuilds it. Its data region is refreshed by the schedule, and its layout is edited in the portal like any document.

The supplier quote margin dashboard: four figures across the top, a bar chart of weighted cost change by category, and a table of the quoted SKUs whose projected margin falls under 20 percent.
The dashboard from 504, built once by hand. The script-published one carries the same layout with a data region the schedule refreshes, so the second-week numbers land in the same shape every Monday without anyone rebuilding the page.

The other pattern: a document that references a file

There is a second way to keep a document current, and it is the one 504 introduced: the document does not carry its data at all. It names a managed resource by its address and re-reads it on every open. A script keeps that file current by rewriting it, and every document naming it shows the new content without being re-saved.

The two patterns look alike from the reader’s chair and behave differently in the ways that matter: what a version holds, and how many documents follow one refresh.

Two ways a dashboard stays current

publish_dataA referenced file
What moves on a run
The JSON inside the document’s data region. The markup is untouched.
The referenced file, rewritten with replace_content. The document is not touched at all.
What a version holds
A complete as-of snapshot: an old version of the dashboard shows the numbers it showed then.
A pointer. Open a six-month-old version of the document and it reads today’s file.
Who edits the layout
Anyone with edit rights, in the portal, like any document. The schedule keeps refreshing only the numbers.
The same, and the data half is edited by whatever rewrites the file.
How many documents follow one refresh
One: the named output.
Every document that names the file, without any of them being re-saved.
Choose it when
One dashboard at one address whose history should be exact, whose layout a person may edit.
Several documents should read one live file, or a table over the file should stay current (see below).

The referencing pattern is the one from 504: a document names a managed resource by its mcp:// address and re-reads it on every open. A script keeps that file current with platform.call("manage_resource", {"action": "replace_content", …}), which keeps the file’s id, address, and name, so nothing that names it has to change.

Keeping a registered table current from a script

The same rewrite that keeps a referencing document current leaves a registered table behind, because a new revision of a file is a new place and the table keeps reading the old one. A script that rewrites a registered file registers it again in its next lines, under the same name, which replaces the registration and points the table at the new revision.

Refreshing a file and moving its table forward, from a script

platform.call("manage_resource", {
    "action": "replace_content",
    "reference": "mcp:resource:71131796b81fbb5641bdcbb3df0ef238",
    "content": sheet_csv,
    "change_summary": "Price sheet for " + month_start,
})

platform.call("manage_table", {
    "action": "register",
    "reference": "mcp:resource:71131796b81fbb5641bdcbb3df0ef238",
    "connection": "scratch",
    "table_name": "supplier_price_sheet",
})

A new revision of a file leaves a registered table behind, exactly as it did in 505, so a script that rewrites the file registers the same name again on the same connection in its next lines. Both are ordinary tool calls the script’s author could make at a prompt, authorized the same way. Neither appears in the run’s output list, because only platform.export and platform.publish_data are outputs; a write made by tool call is recorded in the audit log instead.

Versions

Every refresh is a version, and a schedule produces versions on a cadence. The asset’s owner decides how many it keeps.

Delivering to another system

Some output exists to be consumed somewhere else: the weekly CSV another system picks up from a bucket. A script delivers the same bytes there by naming a destination, a named drop Deasil sets up for your deployment on request, and a key beneath it. Nothing else about the drop is the script’s business.

One result, two destinations

rows = platform.query(connection = conn, sql = "SELECT ...")["rows"]

# The portal asset, refreshed: a new version of one asset.
platform.export(name = "weekly-sales", rows = rows, format = "csv")

# The same bytes, delivered for another system to read.
platform.export(
    name = "weekly-sales",
    rows = rows,
    format = "csv",
    destination = "acme-drop",
    key = "2026/08/sales.csv",
)
  1. 01

    The script names only the destination and the key. The bucket, the prefix, and the credentials belong to the destination, a named drop Deasil sets up for your deployment on request; nothing about them appears in the source.

  2. 02

    Destination and key are passed by name, never by position, so where a script writes can be read from its source by validate before it ever runs.

  3. 03

    One output name may be written once per destination in a run, which is what lets one result go to the portal and to the drop; a second write to the same destination fails rather than silently keeping one of the two.

  4. 04

    A key that could climb out of the drop’s prefix is refused rather than cleaned up, and two outputs may not land on one object.

  5. 05

    Each delivery is recorded on the run: destination, bucket, key, and bytes. A run that is interrupted and resumed does not deliver the same object twice.

Where the outputs go from here

Every output in this lesson is an ordinary asset once it lands: shareable, versioned, referenceable from other documents, and findable by search. The 300 series covers what happens to an asset after that; nothing about a script-written one is different.

Key terms

Five terms cover what a run writes and where it goes.

Key Terms

Output
Something a run declares with platform.export or platform.publish_data. Recorded on the run by name, with the asset and version it produced. A write made by any other tool call is audited but is not an output.
Output identity
The pair of script and output name, which maps to one asset. A stable name refreshes that asset with a version per run; a dated name creates an asset per run.
Data regionid="data"
The one element of a document whose interior platform.publish_data replaces with the run’s JSON. The document’s own code reads it at view time, so the layout and the numbers change independently.
Semi-dynamic dashboard
A document published once whose data region a schedule refreshes. Every version is an as-of snapshot, and a layout edit made in the portal survives the next fire.
Destination
Where an output lands: the portal by default, or a named drop Deasil sets up for your deployment. The script names the destination and a key; the bucket and its credentials are never in the source.