Skip to content

Output Formats

Add @format to any method to control how its return value is displayed. Without it, Photon renders JSON. With it, you get tables, charts, diagrams, dashboards, and more.

Every format works on all three surfaces — Beam renders rich HTML, the CLI renders ASCII/text, and MCP returns structured data with format hints for the client.

Photon format rendering: metrics, gauges, progress bars, and charts

Tables

@format table

The default for arrays of objects. Sortable columns, paginated rows, and expandable row details (click any row to see all fields).

typescript
/** @format table */
users() {
  return [
    { name: "Alice", email: "alice@co.com", role: "admin", joined: "2025-01-15" },
    { name: "Bob", email: "bob@co.com", role: "user", joined: "2025-03-20" },
  ];
}

Auto-detected cell rendering: dates are formatted, URLs become links, status fields get colored badges.

Column Format Pipes

Apply per-column formatting with the @columnFormats hint:

typescript
/** @format table {@columnFormats revenue:currency,margin:percent,name:truncate(20)} */
sales() {
  return [
    { name: "Enterprise North America", revenue: 142830, margin: 0.234 },
    { name: "Mid-Market Europe", revenue: 98450, margin: 0.182 },
  ];
}
PipeEffectExample
currencyLocale currency (default USD)$142,830.00
percentPercentage (values 0-1 are auto-scaled)23.4%
dateLocale date formatJan 15, 2025
truncate(N)Cut to N characters with ellipsisEnterprise Nor...
numberLocale number grouping142,830
compactK/M/B abbreviation142.8K

Lists

@format list

Styled list with title, subtitle, icon, and badge fields.

typescript
/** @format list {@title name, @subtitle email, @badge role, @icon avatar} */
team() { return this.members; }
HintDescription
@titlePrimary text field
@subtitleSecondary text
@iconLeading image/avatar
@badgeStatus badge
@styleplain, grouped, inset, inset-grouped

Cards and Key-Value

@format card

Single object rendered as a styled card.

@format kv

Key-value pairs layout — good for settings, metadata, or single-record display.

@format grid

Array rendered as a visual grid. Use {@columns 3} to control column count.

@format chips

Array rendered as inline chip/tag elements.

@format tree

Hierarchical/nested data rendered as an expandable tree.


Charts

All chart types use Chart.js. Photon auto-detects the best chart type from your data shape, or you can specify explicitly.

@format chart:bar

typescript
/** @format chart:bar {@label region, @value revenue} */
revenueByRegion() {
  return [
    { region: "Americas", revenue: 45000 },
    { region: "Europe", revenue: 38000 },
    { region: "Asia", revenue: 52000 },
  ];
}

@format chart:line

Auto-detected for time series data (fields named date, timestamp, createdAt, etc.).

typescript
/** @format chart:line */
signups() {
  return [
    { date: "2025-01-01", count: 120 },
    { date: "2025-02-01", count: 185 },
    { date: "2025-03-01", count: 240 },
  ];
}

@format chart:pie

Auto-detected for 2-field arrays (one label + one number) with 8 or fewer items.

@format chart:scatter

XY scatter plot. Auto-detected when data has 2+ numeric fields and no string fields.

typescript
/** @format chart:scatter {@x height, @y weight} */
measurements() {
  return [
    { height: 170, weight: 68 },
    { height: 185, weight: 82 },
    { height: 162, weight: 55 },
  ];
}

@format chart:radar

Radar/spider chart for multi-dimensional comparison. Auto-detected for single items with 5+ numeric fields, or few items with many numeric dimensions.

typescript
/** @format chart:radar */
skills() {
  return [{ name: "Alice", coding: 9, design: 6, leadership: 7, communication: 8, testing: 8 }];
}

@format chart:histogram

Bins numeric values into a frequency distribution bar chart. Explicit only — not auto-detected.

typescript
/** @format chart:histogram {@x responseTime} */
latencyDistribution() {
  return this.requests.map(r => ({ responseTime: r.ms }));
}

Other chart types

chart:area (line with fill), chart:donut (doughnut), chart:hbar (horizontal bar).

Chart Hints

HintDescription
@labelCategory/x-axis field
@valueY-axis/size field
@xExplicit x-axis field
@yExplicit y-axis field
@seriesField to group into multiple datasets

Auto-Detection Rules

Data shapeDetected type
Date field + numeric fieldline
2+ numeric fields, no stringsscatter
2 fields (1 string + 1 number), ≤8 itemspie
1 item with 5+ numeric fieldsradar
≤5 items with 4+ numeric + 1 stringradar
Everything elsebar

Single Values

@format metric

Big number display with optional label, delta, and trend arrow.

typescript
/** @format metric */
revenue() {
  return { value: 142830, label: "Revenue", delta: "+12%", trend: "up" };
}

Data shape: { value, label?, delta?, trend? }trend is "up", "down", or "neutral". A raw number also works.

@format gauge

Semicircular gauge with color gradient (green → yellow → red).

typescript
/** @format gauge {@min 0, @max 100} */
cpuUsage() {
  return { value: 73, label: "CPU" };
}

@format ring

Full-circle progress ring with center value text.

typescript
/** @format ring */
uploadProgress() {
  return { value: 73, max: 100, label: "Upload" };
}

Data shape: a number (0-100), { value, max?, label? }, or { progress } (0-1 normalized).


Text and Diagrams

@format markdown

Renders markdown with full formatting support.

@format mermaid

Renders Mermaid diagrams from a string. Supports flowcharts, sequence diagrams, Gantt charts, and more.

typescript
/** @format mermaid */
architecture() {
  return `graph LR
    A[Client] --> B[Photon]
    B --> C[CLI]
    B --> D[Beam]
    B --> E[MCP]`;
}

@format code / @format code:python

Syntax-highlighted code block. Append a language name for specific highlighting.

@format slides

Marp-style slide presentation. Separate slides with ---. Supports layouts, transitions, and embedded photon output.


Dashboards and Containers

@format dashboard

Returns an object where each key becomes a panel. Photon auto-detects the best renderer for each value (numbers → metrics, arrays → tables, etc.).

typescript
/** @format dashboard */
overview() {
  return {
    revenue: { value: 142830, delta: "+12%" },
    users: [{ name: "Alice", role: "admin" }, { name: "Bob", role: "user" }],
    uptime: { progress: 0.997 },
  };
}

Container Formats

Wrap multiple sections with layout control:

FormatDescription
panelsCSS grid of titled panels
tabsTab bar switching between sections
accordionCollapsible sections
stackVertical stack with spacing
columnsSide-by-side columns (2-4)

Data is an object — keys become section titles/tab labels, values are rendered using auto-detected formats.


Specialty Formats

FormatDescriptionData Shape
qrQR codeURL string or { url }
timelineVertical event timeline[{ date, title, description? }]
cartShopping cart with totals{ items: [{ name, price, quantity }] }
checklistInteractive checklist[{ text, done }]

Layout Hints Reference

Hints are specified in curly braces after the format name:

typescript
/** @format chart:bar {@label category, @value amount} */
/** @format list {@title name, @subtitle email, @style inset} */
/** @format table {@columnFormats price:currency,rate:percent} */
/** @format gauge {@min 0, @max 200} */
HintUsed byDescription
@titlelist, gauge, ring, timelinePrimary text field or label
@subtitlelistSecondary text field
@iconlistLeading image field
@badgelistStatus badge field
@stylelistLayout style variant
@columnsgridNumber of columns
@labelchartCategory/x-axis field
@valuechartY-axis/size field
@xchart, histogramX-axis or binning field
@ychartY-axis field
@serieschartMulti-dataset grouping field
@mingaugeMinimum value (default: 0)
@maxgauge, ringMaximum value (default: 100)
@datetimelineDate field name
@descriptiontimelineDescription field name
@columnFormatstablePer-column format pipes
@innercontainersForce inner renderer type
@filtertable, listEnable client-side filtering

Declarative UI (A2UI v0.9)

@format a2ui

Emits an A2UI v0.9 JSONL message stream derived from the return value. A2UI is Google's framework-agnostic declarative UI protocol; it rides on top of AG-UI, which Photon already speaks, so any AG-UI consumer that understands A2UI can render Photon output with no custom integration.

typescript
/** @format a2ui */
async list() {
  return [
    { name: 'Alice', role: 'Eng' },
    { name: 'Bob', role: 'PM' },
  ];
}

Auto-mapping (v1, Basic catalog only):

Return shapeA2UI layout
Array of objectsList with a Card template row
Single objectColumn of Text rows (one per key)
{ title, description, actions: [...] }Card with action buttons
PrimitiveSingle Text component

Escape hatch. For full control, return the verbatim shape:

typescript
/** @format a2ui */
async surface() {
  return {
    __a2ui: true,
    components: [
      { id: 'root', component: 'Card', child: 'header' },
      { id: 'header', component: 'Text', text: 'Custom', variant: 'h1' },
    ],
    data: {},
  };
}

How it ships across transports:

  • CLI: prints the JSONL sequence (one message per line).
  • MCP / AG-UI: each A2UI message is broadcast as an AG-UI CUSTOM event named a2ui.message. A consumer reassembles the JSONL stream. This is the primary integration path — paste the captured stream into A2UI Theater to see it render.
  • Beam: native renderer that walks the component tree, binds JSON Pointer values, and dispatches button actions back to the photon (see "Action round-trip" below).

Action round-trip — button name === method name

When a Beam user clicks an A2UI Button, the renderer dispatches an a2ui:action event and the Beam result-viewer routes it to the same photon by calling the method whose name matches action.event.name. No new contract — write a method, name a button after it, the click hits it.

typescript
/**
 * Stateful counter — view() returns the surface; increment() and reset()
 * are the action handlers. Each one returns the same surface shape, so
 * Beam re-renders in place after the click.
 *
 * @stateful
 */
export default class Counter {
  /** @format a2ui */
  async view() { return this.surface(await this.read()); }

  /** @format a2ui */
  async increment() {
    const next = (await this.read()) + 1;
    await this.memory.set('value', next);
    return this.surface(next);
  }

  /** @format a2ui */
  async reset() {
    await this.memory.set('value', 0);
    return this.surface(0);
  }

  private surface(value: number) {
    return {
      __a2ui: true,
      components: [
        { id: 'root', component: 'Card', child: 'col' },
        { id: 'col', component: 'Column', children: ['v', 'inc', 'reset'] },
        { id: 'v', component: 'Text', variant: 'h1',
          text: { call: 'formatString', args: { value: 'Value: ${/value}' } } },
        { id: 'inc', component: 'Button', text: 'Increment',
          action: { event: { name: 'increment' } } },
        { id: 'reset', component: 'Button', text: 'Reset',
          action: { event: { name: 'reset' } } },
      ],
      data: { value },
    };
  }
}

Action context: any TextField a user has edited gets captured into a local data-model snapshot and shipped as the call's arguments, so a Button { event: { name: 'submit' } } clicked next to TextField { value: { path: '/email' } } calls photon.submit({ email: '...' }).

Non-goals: stateful surface lifecycle across turns (each call replaces the surface), custom catalogs, two-way binding for non-TextField inputs.


For the complete tag reference including non-format tags (caching, validation, middleware, MCP annotations), see Tag Reference.

Released under the MIT License.