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

# Commands, message actions, events and routes

> Write slash commands, message actions, lifecycle events, HTTP routes and MCP servers as Effect handlers.

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

These pieces take the same options and answer with the same shapes as on the Promise API. The
examples use the Effect template's `Tasks` service. For slow work, see
[Deferred answers](/effect/deferring).

## Commands

`defineCommand(name, options, handler)` adds a slash command. The options are `description`,
`options`, `scopes`, `blocks`, `env`, `config` and `progress`, as on the
[Promise API](/commands#options). `options` is a `Schema.Struct`, and its fields appear in the
composer in the order you write them. Give every field a `description` annotation, which the member
sees in the composer. The handler gets the decoded values:

```tsx src/taskCommand.tsx theme={null}
import { Bold, Paragraph } from "@reintersect/sdk/components";
import { defineCommand, member } from "@reintersect/sdk/effect/app";
import { Schema } from "effect";
import { InstallConfig } from "./schemas";
import { TaskBlock } from "./taskBlock";
import { Tasks } from "./tasks";

export const TaskCommand = defineCommand(
  "task",
  {
    description: "Add a task and share it here",
    options: Schema.Struct({
      title: Schema.String.annotations({ description: "What needs doing" }),
      assignee: Schema.optional(member({ description: "Who picks it up" })),
    }),
    config: InstallConfig,
  },
  function* ({ config, options }) {
    const tasks = yield* Tasks;

    const task = yield* tasks.add(options.title, options.assignee?.name);

    return {
      visibility: "public",
      content: (
        <Paragraph>
          Added <Bold>{task.title}</Bold> to {config.project}.
        </Paragraph>
      ),
      block: TaskBlock.insert({ id: task.id, title: task.title }),
    };
  },
);
```

Add it with `.addCommand(TaskCommand)`. The answer can be `public`, `private` or a notice on its own;
see [Answers](/commands#answers). Reintersect waits up to 30 seconds for it.

A union of `Schema.Literal`s is a choice; give each literal a `title` annotation for its label, or
use `choice({ value: "Label" }, { description })`. `member({ description })`, `circle({ description })`
and `appObject(kind, { description })` from `@reintersect/sdk/effect/app` add pickers whose values
are `{ kind: "member", memberId, name }` and so on. To receive only the ID, annotate a string
instead: `Schema.String.annotations({ description: "Owner", jsonSchema: memberOption })`.

Put the `description` on the field's own schema. `JSONSchema.make` drops a description set on a
property signature that has a default, such as `Schema.optionalWith(Schema.String, { default })`, so
annotate the inner schema instead. The value table and the rules for unsupported shapes are in
[Option schemas](/commands#option-schemas).

## Message actions

`defineMessageAction(id, options, handler)` adds an item to the menu on a message. The options are
`label`, `icon`, `context`, `scopes`, `blocks`, `env` and `config`, as on the
[Promise API](/message-actions#options):

```tsx src/saveAsTask.tsx theme={null}
import { defineMessageAction } from "@reintersect/sdk/effect/app";
import { Tasks } from "./tasks";

export const SaveAsTask = defineMessageAction(
  "save-as-task",
  { label: "Save as a task", icon: "list-todo" },
  function* ({ message }) {
    const tasks = yield* Tasks;

    const task = yield* tasks.add(message.text);

    return { notice: `Saved as ${task.id}` };
  },
);
```

Add it with `.addMessageAction(SaveAsTask)`. With `context: "thread"` or `context: "conversation"`,
the handler also gets the thread or conversation in `context`; see
[Context](/message-actions#context). The client in a message action always holds
`conversations:read`.

## Progress updates

A command or message action can return a `Stream` of answers instead of one answer. The first
element is the answer, and every later element replaces it in place, so a block can move through the
stages of long work:

```tsx src/deployCommand.tsx theme={null}
import { defineCommand } from "@reintersect/sdk/effect/app";
import { Effect, Schema, Stream } from "effect";
import { Deploy } from "./deployBlock";
import { Deploys } from "./deploys";

export const DeployCommand = defineCommand(
  "deploy",
  {
    description: "Deploy a service",
    options: Schema.Struct({ service: Schema.String.annotations({ description: "Service" }) }),
  },
  ({ options }) =>
    Stream.unwrap(
      Effect.map(Deploys, (deploys) =>
        Stream.map(deploys.stages(options.service), (status) => ({
          visibility: "public" as const,
          block: Deploy.insert({ service: options.service, status }),
        })),
      ),
    ),
);
```

Services the stream needs and the ways it can fail become the command's requirements and errors, as
they do for an Effect handler. Mark `visibility` `as const` inside `Stream.map`, or the answer won't
type-check. A failure or defect in the stream finishes the answer with the failure notice, and a later
element that changes the visibility or the block dies with `ReintersectAppProgressMismatchError`.
Delivery, coalescing and limits work as described in [Progress updates](/progress-updates).

In tests, `host.settle` runs the stream to the end on the `TestClock`, moving the clock through the
SDK's waits, and `host.answersOf(interactionToken)` lists every answer it sent:

```ts theme={null}
const outcome = yield* host.runCommand("deploy", { service: "api" });

yield* host.settle;

const { answers, final } = yield* host.answersOf(outcome.interactionToken);
```

## Events

`defineEvent(name, handler)` or `defineEvent(name, options, handler)` handles a lifecycle event. The
payload's fields arrive at the top of the context, typed for the event:

```tsx src/onDecision.tsx theme={null}
import { Paragraph } from "@reintersect/sdk/components";
import { defineEvent } from "@reintersect/sdk/effect/app";

export const OnDecision = defineEvent(
  "decision.published",
  { scopes: ["replies:write"] },
  function* ({ decision, reintersect }) {
    yield* reintersect.replies.create({
      conversationId: decision.conversationId,
      content: <Paragraph>Logged "{decision.title}" in the changelog.</Paragraph>,
    });
  },
);
```

Add it with `.addEvent(OnDecision)`. An event handler returns nothing. If it fails, the delivery
fails and Reintersect retries it with backoff, so make the handler safe to run twice. See
[Delivery](/events/overview#delivery) and the [event catalog](/events/catalog).

## Routes

`defineRoute({ method, path, scopes, blocks, env, handler })` serves your own HTTP endpoint, such as a
webhook from another service. The handler returns a `Response`. Routes belong to the app, not to an
install, so the context has `reintersect.forInstall(installId)` in place of a client:

```ts src/ciWebhook.ts theme={null}
import { defineRoute } from "@reintersect/sdk/effect/app";
import { Ci } from "./ci";
import { TaskBlock } from "./taskBlock";

export const CiWebhook = defineRoute({
  method: "POST",
  path: "/webhooks/ci/:installId",
  scopes: ["blocks:write"],
  blocks: { TaskBlock },
  handler: function* ({ params, reintersect, request }) {
    const ci = yield* Ci;

    const event = yield* ci.verify(request);

    yield* reintersect.forInstall(params.installId).blocks.update(TaskBlock, event.blockId, {
      state: { status: event.passed ? "done" : "doing" },
    });

    return new Response(null, { status: 204 });
  },
});
```

`params` is typed from the path. Calling a client method whose scope the route doesn't declare is a
type error, as it is for every piece. A failed route answers `500` with no details. See
[HTTP routes](/hosting/routes) for where routes are served.

## MCP servers

`.addMcp(handler, { auth })` works the same on an Effect app. `ReintersectApp.layer` mounts the
handler at `/mcp` next to the delivery path. See [MCP servers](/mcp/servers).


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