> ## 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.

# Actions

> What your team can do to a record, with the guardrails written next to it.

Actions are the verbs of a resource. Each one runs inside your app when someone in your team (or an agent, or a triage rule) runs it from Pult. Pult never touches your database; it asks, and your code decides.

```ts theme={null}
export const user = resource("user", {
  get: (id) => db.users.find(id),
  title: (u) => u.username,
  actions: (action) => ({
    ban: action({
      input: z.object({
        reason: z.string().min(3).describe("Shown to the user"),
        days: z.number().int().positive().optional()
          .describe("Leave empty for a permanent ban"),
      }),
      when: (u) => u.bannedAt === null,
      danger: "reauth",
      approval: true,
      run: ({ record, input, actor }) =>
        banUser(record.id, { ...input, by: actor.name }),
    }),
    unban: action({
      when: (u) => u.bannedAt !== null,
      run: ({ record }) => unbanUser(record.id),
    }),
  }),
})
```

The key (`ban`) is the action's identity. It's referred to elsewhere as `user.actions.ban`.

## Running

`run` receives:

* `record`: the record, freshly fetched with `get`.
* `input`: the form values, validated against `input` and typed from it.
* `actor`: who ran it, with `name`, `type` (`user`, `agent` or `triage`) and their role.
* `item`: the id of the item it was run from, if any.

Return nothing, a message, or a message with blocks:

```ts theme={null}
run: ({ record }) => `Refunded ${formatMoney(record.total)}`,
run: async ({ record }) => ({
  message: "Invoice sent",
  blocks: [b.attachment({ name: "invoice.pdf", url })],
}),
```

The result shows in a toast and in the item's or record's history. Throw an error to fail: its message is shown to the person and recorded too.

Actions always wait for your app. The console says it's waiting for your app to confirm, and if your app is offline the action is queued and marked as waiting until your app answers.

## Forms

`input` is a schema, and Pult builds the form from it: text fields, numbers, dates, switches, single and multiple choice from enums, and multi-line boxes for strings longer than 200 characters. `.describe()` adds a hint under a field. An action without `input` runs straight away (unless it needs confirmation).

`defaults(record)` prefills the form from the record:

```ts theme={null}
setSeats: action({
  input: z.object({ seats: z.number().int().min(1) }),
  defaults: (org) => ({ seats: org.seats }),
  run: ({ record, input }) => db.orgs.setSeats(record.id, input.seats),
}),
```

`pick` turns a field into a searchable record picker, and `choices` fills a dropdown from your app:

```ts theme={null}
transfer: action({
  input: z.object({ owner: z.string() }),
  pick: { owner: "user" },
  run: ({ record, input }) => db.repos.transfer(record.id, input.owner),
}),
changePlan: action({
  input: z.object({ plan: z.string() }),
  choices: { plan: (org) => billing.plansFor(org) },
  run: ({ record, input }) => billing.change(record.id, input.plan),
}),
```

Choices can be strings or `{ value, label }` pairs. The history shows a picked record by its title.

## When an action applies

`when(record)` hides the action unless it applies right now: Ban only for users who aren't banned, Unban only for those who are. Your app checks it again when the action runs, so an action that stopped applying in the meantime is refused with a clear message.

## Guardrails

`danger: "confirm"` asks for confirmation first. `danger: "reauth"` also asks the person to sign in again if they haven't in the last ten minutes. Dangerous actions are shown in red.

`approval: true` means one person asks and someone else approves. The request waits under Approvals; the person who asked can't approve it themselves, and approving a `reauth` action needs a fresh sign-in too. The history shows who asked, who approved and what ran.

`preview` shows what would happen before anyone commits:

```ts theme={null}
preview: ({ record }) => [
  b.markdown(`**${record.username}** loses ${countRepos(record)} repos`),
],
```

## Background actions

Long work (exports, migrations, bulk changes in your own system) can run in the background:

```ts theme={null}
exportActivity: action({
  background: true,
  run: async ({ record, progress }) => {
    for (const [index, month] of months.entries()) {
      await exportMonth(record.id, month)
      await progress((index + 1) / months.length, `${month} done`)
    }
    const csv = { name: "activity.csv", contentType: "text/csv", data }
    return { message: "Export ready", blocks: [b.attachment(csv)] }
  },
}),
```

The console doesn't wait. The history shows a progress bar with your notes, then the result. `progress` takes a fraction between 0 and 1 (or `null` when you don't know) and an optional note.

## Actions without a record

Some work isn't about one record: purging a cache, re-running last night's payouts, sending a digest. Define those on their own:

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

export const purgeCache = action("purge-cache", {
  label: "Purge CDN cache",
  input: z.object({ path: z.string().default("/") }),
  danger: "confirm",
  run: ({ input, actor }) => cdn.purge(input.path, { by: actor.name }),
})
```

They take the same options as any action except `when`, and `record` is `null` when they run. Grant them in a role like other actions. People run them from ⌘K, from [pages](/build/pages) and from any block with `b.actions(purgeCache)`.

## On a schedule

An action without a record can also run by itself:

```ts theme={null}
export const weeklyDigest = action("weekly-digest", {
  schedule: { cron: "0 9 * * 1", timezone: "Europe/Amsterdam" },
  run: () => sendDigest(),
})
```

`schedule` is a cron expression, or an object with one and a timezone. Without a timezone it runs in UTC. Each environment runs its own schedule, and every run is in the audit log under "Schedule" with its result. If your app can't be reached when a run is due, that run is recorded as failed instead of piling up for later.

People can still run a scheduled action by hand. `pult check` makes sure scheduled actions need no input without a default and no approval, since nobody is there to give them.

## Triage and agents

Triage rules can run actions that are harmless (`danger: "none"`) or that you mark `automatable: true`. They never run actions that need an approval. Agents run actions through their role like people do, but can't run `reauth` actions, since they can't sign in again.

## Running on many at once

People can run an action on many records from a resource's table, or on the records of many items from a list. Each one runs separately in your app and the result says how many succeeded.


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