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

# Blocks

> Interactive cards your app places in conversations. You define the props, state and view; Reintersect stores, renders and syncs them.

> Reintersect apps are in developer preview. Names and limits can still change before general
> release.

A block is a card your app places inside a post, reply or direct message. It can show live data,
collect votes, run actions and update in place for everyone in the conversation. Members insert
blocks from the slash menu, agents insert them as tools, and your app inserts them from events,
message actions or the API.

Using Effect? See [Blocks and references](/effect/blocks-and-references) in the Effect guide.

You describe a block with three things:

| Part | What it is |
| - | - |
| Props | The input the block was created with, such as a task ID. Props never change. |
| State | Data Reintersect stores for this one block, such as `status: "done"`. Actions return the next state. |
| View | A render function that turns props and state into JSX built from the [component catalog](/components/overview). |

## Define a block

This is the block `npm create reintersect` scaffolds, shown with Zod:

```tsx src/taskBlock.tsx theme={null}
import { defineAction, defineBlock } from "@reintersect/sdk/app";
import { Actions, Block, Button, Fact, Facts, Header, Status } from "@reintersect/sdk/components";
import { z } from "zod";
import { updateTaskStatus } from "./tasks";

const TaskStatus = z.enum(["todo", "doing", "done"]);
const TaskState = z.object({ status: TaskStatus });

const LABELS = { todo: "To do", doing: "In progress", done: "Done" } as const;
const STATES = { todo: "neutral", doing: "running", done: "success" } as const;
const NEXT = {
  todo: { status: "doing", label: "Start" },
  doing: { status: "done", label: "Mark done" },
  done: { status: "todo", label: "Reopen" },
} as const;

export const moveTask = defineAction(
  "move",
  {
    args: z.object({ id: z.string(), status: TaskStatus }),
    state: TaskState,
    description: "Move the task to another status",
  },
  async ({ args }) => {
    await updateTaskStatus(args.id, args.status);

    return { state: { status: args.status }, notice: `Moved to ${LABELS[args.status]}` };
  },
);

export const TaskBlock = defineBlock({
  type: "task",
  title: "Task",
  description: "A task from your tracker, with its status and a button to move it along.",
  icon: "list-todo",
  insertable: true,
  props: z.object({ id: z.string(), title: z.string(), assignee: z.string().optional() }),
  state: TaskState,
  initialState: { status: "todo" },
  actions: { move: moveTask },
}).render((props, { actions, state }) => {
  const next = NEXT[state.status];

  return (
    <Block summary={`${props.id} ${props.title}: ${LABELS[state.status]}`}>
      <Header trailing={<Status state={STATES[state.status]} label={LABELS[state.status]} />}>
        {props.title}
      </Header>
      <Facts>
        <Fact label="Assignee">{props.assignee ?? "Unassigned"}</Fact>
      </Facts>
      <Actions>
        <Button label={next.label} action={actions.move.with({ id: props.id, status: next.status })} />
      </Actions>
    </Block>
  );
});
```

Then add it to your app with `.addBlock(TaskBlock)`. `addBlock` rejects a block without a view, so
call `.render()` first.

Every view follows the same anatomy: a `Header`, the body, one `Actions` row and an optional
`Footer`, in that order. The view says what each part is, and Reintersect decides the spacing, which
button is primary, the colours and how the block adapts to narrow widths. The task's one button is
primary because it's the first in `Actions`. See
[What Reintersect decides](/components/overview#what-reintersect-decides).

## Options

| Option | Type | Description |
| - | - | - |
| `type` | `string` | Unique within the app. 1 to 48 characters from `a-z`, `0-9` and `-`. Changing it creates a different block type. |
| `title` | `string` | Shown in the slash menu, on the consent screen and as the fallback title. Defaults to `type`. |
| `description` | `string` | Shown in the slash menu and given to agents with the insert tool. |
| `icon` | `IconName` | Icon for the slash menu item. |
| `props` | Schema | Validates the props a block is created with, at most 16 KB as JSON. Its JSON Schema becomes the slash menu's props form and the agent insert tool's arguments. |
| `state` | Schema | Validates stored state, at most 64 KB as JSON. Must not transform values. See [State and actions](/blocks/state-and-actions). |
| `initialState` | State | The state a new block starts with. Checked against `state`. |
| `inputs` | `Record<string, choice(…)>` | Per-viewer inputs such as votes. See [Viewer inputs](/blocks/viewer-inputs). |
| `sections` | `string[]` | Names of sections a toggle button can open, at most 32. See [Sections and forms](/blocks/sections-and-forms). |
| `actions` | `Record<string, Action>` | Actions the view can bind to buttons, forms, menus and choice groups. At most 64 per block. |
| `insertable` | `boolean` | Default `false`. When `true`, members see the block in the slash menu and agents get an insert tool. Leave it `false` for blocks only your app inserts. |
| `reopenable` | `boolean` | Default `false`. Lets the creator or an admin reopen the block after an action marks it final. |
| `staleAfter` | `number` | Seconds, from 60 to 30 days. When someone views the block after this long, Reintersect renders it again. |
| `scopes`, `env`, `config` | | What the render function itself uses. See [Composing an app](/concepts/composition). |

Schemas are any [Standard Schema](https://standardschema.dev) that also implements Standard JSON
Schema: Zod 4, ArkType and Valibot (through `toStandardJsonSchema`). The Effect entry takes Effect
Schema. See [Composing an app](/concepts/composition#schemas).

## The render function

`render` receives the block's props and a context object, and returns JSX. It can be `async` and
fetch live data:

```tsx src/deploymentCard.tsx theme={null}
import { defineBlock, secret } from "@reintersect/sdk/app";
import { Block, Header, Status } from "@reintersect/sdk/components";
import { z } from "zod";
import { getDeployment } from "./vercel";

export const DeploymentCard = defineBlock({
  type: "deployment-card",
  title: "Deployment",
  props: z.object({ deploymentId: z.string() }),
  state: z.object({}),
  initialState: {},
  env: { VERCEL_TOKEN: secret() },
  staleAfter: 300,
}).render(async (props, { env }) => {
  const deployment = await getDeployment(props.deploymentId, env.VERCEL_TOKEN.reveal());

  return (
    <Block summary={`${deployment.name} is ${deployment.state}`}>
      <Header href={deployment.url} trailing={<Status state="running" label={deployment.state} />}>
        {deployment.name}
      </Header>
    </Block>
  );
});
```

| Context field | Description |
| - | - |
| `state` | The block's current state. |
| `inputs` | Handles for each declared input, passed to `ChoiceGroup`. |
| `actions` | Bound actions. Pass them to `Button`, `Form`, `Menu`, `Select` and `ChoiceGroup`. |
| `sections` | Handles for each declared section, passed to `Section` and toggle buttons. |
| `env`, `config` | The environment and install config the block declared. |

One render is shared by every viewer. The context has no viewer, so a render can't show different
content to different people. To hide parts of a block from some viewers, use
[audience](/blocks/audience-and-lifecycle#audience); to collect a choice from each viewer, use
[viewer inputs](/blocks/viewer-inputs), whose counts Reintersect shows without a re-render.

### When Reintersect renders

Reintersect calls your render function, validates and stores the resulting tree, and serves that
stored tree to viewers. It renders again when:

* The block is inserted, pasted or cloned.
* An action returns. The SDK renders with the new state and returns both in the same response.
* Your app pushes new state or asks for a revalidation through the API.
* Someone views the block after `staleAfter` has passed.

Viewers never wait on your server to see a block. If a render fails or times out, Reintersect keeps
the last good tree. See [Rendering](/blocks/rendering).

## Where blocks come from

| Source | How |
| - | - |
| Slash menu | Members type `/` and pick the block's title. Every `insertable` block type is its own item. When the props schema has required fields, Reintersect first shows a form generated from the schema's titles and descriptions. |
| Agents | Each `insertable` block becomes an insert tool. The agent prepares the block and the member places it. See [Agents](/agents). |
| Commands and message actions | Answer with `{ visibility, block: TaskBlock.insert(props) }`. See [Slash commands](/commands) and [Message actions](/message-actions). |
| Your code | Call `reintersect.blocks.insert(TaskBlock, { conversationId, props })` from an event, route or job, or with `circleId` to post in a circle's chat. Needs `blocks:write`. See [State and actions](/blocks/state-and-actions#pushing-state-from-your-systems). |
| References | A reference kind can show a block for its links. See [Resolving](/references/resolving). |
| Copy and paste | Pasting a copied block creates a fresh block of the same type and props with `initialState`, and your app receives `block.created` with `origin: "clone"`. |

Blocks inserted through the API land only in circles your app can reach. See
[Install and consent](/concepts/install-and-consent#reach).

## The summary

`Block` requires a `summary` of at most 280 characters of plain text. Reintersect uses it wherever the
full block can't appear: notification text, Slack peers of a connected circle, search results and
the agent's fallback text for the block. Write it as a sentence a teammate could read on its own:

```tsx theme={null}
<Block summary="Launch v2.4: shipping Thursday. Waiting on Ada's review.">
```

## Attribution

Every block shows a footer line with your app's icon and "via \<app name>". Blocks from a
development install also show a "Dev" label. You control the header, but you can't remove or
restyle the attribution line.

## Next steps

<Columns cols={2}>
  <Card title="State and actions" icon="zap" href="/blocks/state-and-actions">
    Define actions, bind them to buttons and return the next state.
  </Card>

  <Card title="Viewer inputs" icon="users" href="/blocks/viewer-inputs">
    Votes and choices stored per member, with live tallies.
  </Card>
</Columns>


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