Guides

Publish a refreshable report

Turn database results into a Markdown report you can open in your browser.

A report runs a JavaScript script and saves the Markdown it returns. For example, an issue report can list work for a human to review. Opening the report shows its last successful result while a refresh runs.

Scripts can read the local database through SQL or saved queries. They can also load Node modules, so only run scripts you trust.

Caution

Validating, saving, refreshing, or opening a report executes its script with Silo's access to your machine. A script can read or change files and use the network. Inspect reports from other authors before running them.

Define and save a report

This example assumes the issues table from Getting started. Save this definition as issue-brief.json:

{
  "slug": "issue-brief",
  "title": "Project issue brief",
  "script": "const issues = silo.sql('SELECT id, title FROM issues ORDER BY title')\n\nreturn [\n  '# Project issue brief',\n  issues.rows.length ? markdown.table(issues) : '_No issues._',\n].join('\\n\\n')"
}

Validate it, save it, then inspect the result:

silo report validate --file issue-brief.json
silo report put --file issue-brief.json
silo report show issue-brief

The output of report show should include your issue in a Markdown table. If the table is empty, it shows _No issues._.

report validate runs the candidate without saving it. It does not prevent side effects from the script itself.

report put runs the script before replacing the stored definition and rendering. If the script throws or returns an invalid value, an existing report with the same slug remains unchanged.

A script must return a Markdown string synchronously. Do not use top-level await or return a promise. This lets Silo keep the database reads and saved result in one SQLite transaction.

Use the report script API

Silo calls the stored script as a function body with three arguments:

Name Purpose
silo.workspace The Git workspace's root, identity, and origin.
silo.sql(sql, parameters?) Runs one bounded read-only SQL statement. Parameters may be a named object or positional array.
silo.query(name, parameters?) Runs a saved query through its typed parameter contract.
markdown.table(result) Renders a query result as a GitHub-flavored Markdown table.
require Loads synchronous Node modules and repository dependencies relative to the workspace root.

Both query methods return:

{
  columns: string[]
  rows: unknown[][]
  truncated: boolean
}

Each call returns at most 500 rows. To show a warning when more rows match, replace issue-brief.json with this version, then save it with silo report put --file issue-brief.json:

{
  "slug": "issue-brief",
  "title": "Project issue brief",
  "script": "const issues = silo.sql(\n  \"SELECT id, title FROM issues WHERE title LIKE :prefix || '%' ORDER BY title\",\n  { prefix: 'Document' },\n)\n\nconst body = issues.rows.length ? markdown.table(issues) : '_No matching issues._'\nconst warning = issues.truncated ? '> Results truncated to 500 rows.' : ''\nreturn ['# Project issue brief', body, warning].filter(Boolean).join('\\n\\n')"
}

This version lists titles beginning with Document. With fewer than 501 matches, it shows no truncation warning.

silo.sql is read-only and cannot access Silo's internal tables. The script itself can still use Node APIs directly.

Reuse a saved query

After defining find-issues in Run saved queries, you can use this script body to reuse it:

const issues = silo.query('find-issues', { prefix: 'Document' })

return [
  '# Project issue brief',
  issues.rows.length ? markdown.table(issues) : '_No matching issues._',
].join('\n\n')

Each run uses the current saved-query definition. Updating or deleting that query can break the next refresh; Silo does not scan scripts to find their dependencies. A failed refresh keeps the last successful result.

Load repository code

require resolves files and dependencies from the Git workspace root. To try this example, first create reports/render-issue.cjs in that repository:

module.exports = (title) => `- ${title}`

Then use this report script body:

const { format } = require('node:util')
const renderIssue = require('./reports/render-issue.cjs')

const issues = silo.sql('SELECT id, title FROM issues ORDER BY id')
return issues.rows.map((row) => renderIssue(format('%s', row[1]))).join('\n')

The result is a Markdown list of issue titles.

Silo synchronizes the stored script. It does not copy required files or packages. Every machine running this example needs reports/render-issue.cjs. Use synchronous modules and APIs.

Inspect the definition and rendering

To inspect or save the script without its rendered output:

silo report show issue-brief --definition

The definition view shows slug, title, and script in a fenced JSON block. To reuse it as a --file input, copy the JSON without the heading or code fences. Show the last successful result and script together with:

silo report show issue-brief

Open the local viewer

Start the packaged viewer from the associated Git repository:

silo report open issue-brief

The command starts a local server and opens your browser. The page displays the last successful result, then refreshes. It refreshes again whenever the page regains focus. The diagram shows what happens when refresh succeeds or fails:

alt [Script returns Markdown] [Script throws] Open report Last successful Markdown Refresh on load or focus Execute stored JavaScript Run declared reads Store new rendering Replace report and freshness state Keep last good rendering Show stale result and error Browser Local viewer Trusted report script Local database

The viewer displays GitHub-flavored Markdown without executing embedded HTML. The report script runs in the local Silo process. This viewer is for local use; it does not provide remote hosting or an authentication system.

Interrupt the CLI command to stop the server.

Refresh or manage reports from the CLI

Command Result
silo report validate Runs a candidate without saving report state.
silo report list Lists reports and their latest refresh state.
silo report show <slug> Shows the last successful rendering and stored script.
silo report show <slug> --definition Shows the definition in a fenced JSON block.
silo report refresh <slug> Reruns the script and atomically stores a successful result.
silo report put Creates or replaces a definition and performs its initial run.
silo report open <slug> Starts the local viewer and refreshes on page load and focus.
silo report delete <slug> Permanently deletes the definition and rendering.

If a refresh fails, Silo records the error and attempt time while retaining the previous rendering. Fix the script, its dependencies, or its source data. Use report put for a changed script and report refresh when the stored script can succeed without replacement.

Warning

silo report delete is permanent. Save the definition first if you may need it again, for example with silo report show issue-brief --definition > issue-brief-backup.md.

Existing Markdown and query reports

Silo still reads, refreshes, synchronizes, and replaces legacy definitions that contain markdown and queries. That format is deprecated. New reports and bundled templates should use script.

A legacy report keeps its existing behavior, including fixed saved-query bindings, query provenance, query slots, and automatic table formatting. Replacing it with a scripted definition removes its stored query rows after the new script runs successfully.

Share reports through explicit synchronization

Report changes remain local until silo push. Another machine receives them through silo pull. This includes:

  • Scripts
  • Saved Markdown results
  • Refresh status
  • Deletions

Pulling a report stores code but does not execute it. Validating, putting, refreshing, or opening it does.

Important

Opening or refocusing the viewer refreshes the report. In a synchronized Silo, a successful refresh updates report metadata and creates pending local work. Check silo sync status and push when the new snapshot should be shared.

Changes to different reports can be combined. Changes to the same report may conflict. Preserve the script you need, follow the recovery steps in Synchronize a database, then put or refresh the reconciled report.

For failures and stale viewer states, continue with Troubleshooting.