> ## Documentation Index
> Fetch the complete documentation index at: https://docs.pult.sh/llms.txt
> Use this file to discover all available pages before exploring further.

# Blocks

> The pieces items, records, pages and action results are made of.

Everything your app shows in Pult is a list of blocks: an item's content, a record's page, a [page](/build/pages), the result of an action. Every block works in all of them, so a button, a flag or a live count can sit on a user's page as easily as on a dashboard. Build them with `b`:

```ts theme={null}
import { b } from "pult"

render: (item) => [
  b.markdown(`**${item.data.reporter}** reported this comment`),
  b.fields({
    author: b.ref("user", item.data.authorId, item.data.author),
    reports: item.occurrences,
  }),
  b.code(item.data.stack, { lang: "trace" }),
]
```

## Text

`b.markdown(text)` renders Markdown. Raw HTML isn't allowed.

`b.code(code, { lang, title })` shows code or logs with a copy button. `lang: "trace"` highlights the first line of a stack trace and file locations; `lang: "log"` highlights file locations.

## Fields

`b.fields({ label: value })` shows labelled values. A value can be a string, a number, a boolean, a `Date`, or one of these:

* `b.ref(resource, id, title)` links to a record. Items link to every record mentioned in their fields, and offer that record's actions.
* `b.sensitive(value, { hint })` hides a value until someone allowed to reveals it. See [Sensitive values](#sensitive-values).
* `item.occurrences`, `item.users`, `item.firstSeen`, `item.lastSeen` and `item.due` inside `render` are live: they stay current as the item changes.

`undefined` values are left out, so optional fields need no special handling.

`b.stats({ label: value })` shows the same values as large numbers, for the top of a page. A stat can carry a note: `{ value: 41, note: "of 120 seats" }`.

## Editing in place

`b.edit(value, action)` shows a value with a pencil next to it. Clicking it opens the action's form with the value filled in:

```ts theme={null}
b.fields({ plan: b.edit(u.plan, "changePlan") })
```

The value goes into the action's only input field, or the one you name with `{ field }`. Everything about the action still applies: who may run it, its confirmation and its approval.

## Records and tables

`b.record(resource, id, title)` shows a record as a link on its own line.

`b.table(rows, columns)` shows a table. Rows are objects; cells take the same values as fields, including `b.ref` and `b.sensitive`. Columns default to the keys of the first row.

```ts theme={null}
b.table(
  org.members.map((m) => ({
    member: b.ref("user", m.id, m.username),
    plan: m.plan,
    joined: m.createdAt,
  })),
)
```

## Charts and timelines

`b.chart(points, { title })` charts your own numbers, given as `{ at, value }` pairs.

`b.chart("occurrences", { range })` charts how often an item happened over `"1h"`, `"24h"`, `"7d"` or `"30d"`, and `b.chart(inbox, { range })` does the same for everything in an inbox. Pult keeps those numbers; your app only places the chart.

`b.timeline(entries)` shows a list of `{ at, text }` events.

## Buttons

`b.actions(...)` shows buttons that run actions:

```ts theme={null}
b.actions(
  purgeCache,
  { action: user.actions.ban, record: u.id, label: "Ban author" },
  { action: exportOrders, input: { since: "2026-01-01" } },
)
```

Pass an action as it is, or with the `record` it runs on, a `label` and `input` to start the form with. Buttons for actions the viewer can't run are left out. On records and pages, so are buttons whose action doesn't apply to its record right now (its `when` says no), and actions with `defaults` start from them. The same goes for `b.edit`. Items keep their buttons as your app sent them. Pressing one works like anywhere else in the console: it runs straight away, or opens the form when the action has inputs or guardrails.

`b.form(action)` shows an action's form right on the page instead of behind a button, for the things people do there all day, like broadcasting a message. It takes the same `record`, `label` and `input` as a button.

Inside a resource's `render`, refer to the resource's own actions by name, since the resource isn't defined yet while it renders. They run on the record being shown:

```ts theme={null}
render: (u) => [b.actions("restrict", "ban")],
```

## From Pult

These blocks show Pult's own data, worked out when someone looks, so they're always current and only show what the viewer's role allows:

* `b.count(inbox, { filter, label })` and `b.count(view)` show how many items match, linking to the list.
* `b.items(inbox, { filter, limit, label })` and `b.items(view)` show the latest few items.
* `b.flag(flag)` shows a [flag](/build/flags) and its value, with the switch for people allowed to change it.

## Layout

`b.columns([...], [...])` puts lists of blocks side by side, as many columns as you give it. On a phone they stack.

## Attachments

`b.attachment({ name, url })` links to a file your app hosts. To hand Pult the file itself, pass its contents and Pult stores it:

```ts theme={null}
b.attachment({ name: "export.csv", contentType: "text/csv", data: bytes })
```

Files are uploaded when the block is sent, up to 20 MB each, and only people signed in to that environment can open them.

## Sensitive values

```ts theme={null}
const [name, domain] = user.email.split("@")
const hint = `${name[0]}…@${domain}`
b.fields({ email: b.sensitive(user.email, { hint }) })
```

The value never reaches the browser until someone reveals it. People see the hint, or "Hidden" without one. Admins and roles with `reveal: true` can reveal it on items, occurrences, records and pages, and every reveal is recorded on the item or record it came from and in the audit log. In tables of records, action results and history entries, sensitive values stay hidden for everyone. Triage never sees them.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.