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

# Handlers and schemas

> The three forms an Effect handler can take, what's in its context, how Effect Schema decodes and encodes, and how to declare environment values, secrets and install config.

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

Every piece of an Effect app runs your code in a handler: block actions and views, reference
`resolve` and `search`, commands, message actions, events and routes. This page covers what they have
in common. The pages for each piece cover their options.

## Handler forms

A handler takes one of two forms:

* A generator function, `function* (context) { … }`. This is the recommended form: `yield*` runs an
  Effect or reads a service, and the value you `return` is the result.
* A function that returns an `Effect`.

A block's view can also return JSX or a `Promise` of JSX. See
[Blocks and views](/effect/blocks-and-references#blocks-and-views).

These two actions do the same thing:

```ts theme={null}
import { defineAction } from "@reintersect/sdk/effect/app";
import { Effect } from "effect";
import { TaskState } from "./schemas";

export const finish = defineAction("finish", { state: TaskState }, function* () {
  yield* Effect.log("Finishing the task");

  return { state: { status: "done" } };
});

export const finishWithEffect = defineAction("finish-effect", { state: TaskState }, () =>
  Effect.succeed({ state: { status: "done" as const } }),
);
```

`Effect.succeed` widens `"done"` to `string`, which the state schema rejects, so it needs
`as const`. A generator's return value keeps its literal types without it.

### Keep handlers unwrapped

Don't wrap a handler in `Effect.fn`. It hides the context's type, so `args`, `state` and the rest
become `any` and mistakes stop being type errors. `reintersect check` warns about it with
`effect-fn-handler`. To trace a step, add a span to the Effect inside the handler:

```ts theme={null}
function* ({ args }) {
  const tasks = yield* Tasks;

  yield* tasks.updateStatus(args.id, args.status).pipe(Effect.withSpan("tasks.updateStatus"));

  return { state: { status: args.status } };
}
```

## The context

Each handler gets the same context as on the [Promise API](/blocks/state-and-actions#handler-context),
with these differences:

| Field | On the Effect API |
| - | - |
| `reintersect` | The Effect client. Each method returns an `Effect` with the API's tagged errors in its error channel. See [Client and streams](/effect/client-and-streams). |
| `waitUntil(effect)` | Returns an `Effect<void>` to yield. The Effect keeps running after the delivery returns, with the handler's services. If it fails, the SDK logs the cause. |
| `defer(effect, options?)` | Commands and message actions only. Takes an `Effect` that produces the answer and returns an `Effect` to yield. See [Deferred answers](/effect/deferring). |
| `env` | The declared environment values, decoded. `secret()` values are `Redacted<string>`. |
| `config` | The install config, decoded by its schema. |

`waitUntil` suits work the member doesn't wait for, such as recording an audit entry:

```ts theme={null}
function* ({ args, waitUntil }) {
  const audit = yield* Audit;

  yield* waitUntil(audit.record(`Moved ${args.id} to ${args.status}`));

  return { state: { status: args.status } };
}
```

Every other field (`args`, `state`, `tallies`, `actor`, `options`, `message`, `params` and so on) has
the same type and meaning as on the Promise API.

## Schemas

Every schema on the Effect API is an Effect Schema with no requirements, a
`Schema.Schema<A, I, never>`. Passing a Zod, ArkType or Valibot schema is a type error.

The SDK decodes props, action arguments, form input, install config and environment values before
your handler sees them. Block state is decoded before your view and actions read it and encoded back
to JSON before Reintersect stores it. That means block state can use a transforming schema: your code
works with a `Date` and Reintersect stores the ISO string.

```ts theme={null}
import { defineAction, defineBlock } from "@reintersect/sdk/effect/app";
import { Clock, Schema } from "effect";

const LaunchState = Schema.Struct({
  status: Schema.Literal("open", "shipped"),
  shippedAt: Schema.optional(Schema.DateFromString),
});

export const ship = defineAction("ship", { state: LaunchState }, function* () {
  const now = yield* Clock.currentTimeMillis;

  return { state: { status: "shipped", shippedAt: new Date(now) } };
});

export const Launch = defineBlock({
  type: "launch",
  props: Schema.Struct({ title: Schema.String }),
  state: LaunchState,
  initialState: { status: "open" },
  actions: { ship },
});
```

An action returns a partial state. The SDK merges it into the current state, then encodes the result.

The SDK encodes `initialState` when you define the block. A value that doesn't match the schema
throws at startup with `Block "launch" state does not match its schema`.

Forms, agent tools and the manifest use JSON Schema, which the SDK builds with `JSONSchema.make`. A
recursive schema, such as one built with `Schema.suspend`, is rejected when the manifest syncs.

## Environment, secrets and install config

Declare what the app reads on `createReintersectApp`, and declare on each piece what that piece
reads:

```ts src/schemas.ts theme={null}
import { secret } from "@reintersect/sdk/effect/app";
import { Schema } from "effect";

export const InstallConfig = Schema.Struct({ project: Schema.String });

export const env = {
  TRACKER_TOKEN: secret(),
  PAGE_SIZE: Schema.NumberFromString,
};
```

```ts src/syncTasks.ts theme={null}
import { defineAction } from "@reintersect/sdk/effect/app";
import { Redacted } from "effect";
import { env, InstallConfig } from "./schemas";
import { Tracker } from "./tracker";

export const syncTasks = defineAction(
  "sync",
  { env, config: InstallConfig, description: "Sync tasks from the tracker" },
  function* ({ config, env }) {
    const tracker = yield* Tracker;

    const count = yield* tracker.sync({
      project: config.project,
      token: Redacted.value(env.TRACKER_TOKEN),
      pageSize: env.PAGE_SIZE,
    });

    return { notice: `Synced ${count} tasks` };
  },
);
```

Pass the same declarations to the app with
`createReintersectApp({ name: "Tasks", env, installConfig: InstallConfig })`. Adding a piece that
reads an environment value or install config the app doesn't declare is a type error. See
[Environment and config](/concepts/env-and-config) for where the values come from.

Environment values always arrive as strings, so each schema must decode from a string, such as
`Schema.NumberFromString`. A schema that doesn't, such as `Schema.Number`, is a type error that names
the key. For an optional value with a default, decode from `Schema.UndefinedOr(Schema.String)` and
give it a `jsonSchema` annotation, so `reintersect check` can describe it in the manifest:

```ts theme={null}
const TASKS_URL_DEFAULT = "https://tasks.example.com";

const TasksUrl = Schema.transform(
  Schema.UndefinedOr(Schema.String).annotations({
    jsonSchema: { type: "string", format: "uri", default: TASKS_URL_DEFAULT },
  }),
  Schema.String,
  { strict: true, decode: (url) => url ?? TASKS_URL_DEFAULT, encode: (url) => url },
);
```

### Secrets

`secret()` declares a secret environment value. Your handlers receive it as a `Redacted<string>`.
Read it with `Redacted.value(env.TRACKER_TOKEN)` where you need it. Everywhere else, including logs
and string templates, it prints as `<redacted>`.


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