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

# Defining Events

> Describe your events in a Prisma-style schema: payload models, event types, defaults, and wire names. The SDK, the payloads, and your event docs are generated from it.

export const RepostHighlight = () => {
  useEffect(() => {
    const colors = {
      comment: ['#116329', '#6A9955'],
      constant: ['#0550AE', '#9CDCFE'],
      keyword: ['#CF222E', '#569CD6'],
      name: ['#953800', '#4EC9B0'],
      string: ['#0A3069', '#CE9178'],
      type: ['#0550AE', '#4EC9B0'],
      variable: ['#1F2328', '#9CDCFE']
    };
    const tokenPattern = /"[^"\n]*"|@[A-Za-z_]\w*|\b(?:String|Int|Float|Boolean|DateTime|Json)\b|\b(?:true|false)\b|\b\d+(?:\.\d+)?\b|\b(?:now|uuid|cuid)(?=\s*\()|\b[A-Z]\w*\b|\b[A-Za-z_]\w*\b/g;
    const append = (line, text, color) => {
      if (!text) return;
      if (!color) {
        line.append(text);
        return;
      }
      const span = document.createElement('span');
      span.textContent = text;
      span.style.color = color[0];
      span.style.setProperty('--shiki-dark', color[1]);
      line.append(span);
    };
    const commentStart = text => {
      let quoted = false;
      for (let index = 0; index < text.length - 1; index += 1) {
        if (text[index] === '"' && text[index - 1] !== '\\') quoted = !quoted;
        if (!quoted && text[index] === '/' && text[index + 1] === '/') return index;
      }
      return -1;
    };
    const highlightCode = code => {
      if (code.dataset.repostHighlighted !== undefined || code.querySelector('span[style*="--shiki-dark"]')) {
        return;
      }
      code.dataset.repostHighlighted = '';
      const source = code.textContent ?? '';
      const fragment = document.createDocumentFragment();
      for (const text of source.split('\n')) {
        const line = document.createElement('span');
        line.className = 'line';
        const declaration = text.match(/^(\s*)(generator|model|enum|type|event)(\s+)([A-Za-z_]\w*)(.*)$/);
        if (declaration) {
          append(line, declaration[1]);
          append(line, declaration[2], colors.keyword);
          append(line, declaration[3]);
          append(line, declaration[4], colors.name);
          append(line, declaration[5]);
        } else {
          const start = commentStart(text);
          const codeText = start === -1 ? text : text.slice(0, start);
          const leading = codeText.match(/^(\s*)([A-Za-z_]\w*)(?=\s+(?:@|[A-Z]))/);
          const bare = codeText.match(/^(\s*)([A-Za-z_]\w*)(\s*)$/);
          let offset = 0;
          for (const match of codeText.matchAll(tokenPattern)) {
            append(line, codeText.slice(offset, match.index));
            const token = match[0];
            let color = colors.constant;
            if (token.startsWith('"')) color = colors.string; else if (token.startsWith('@') || (/^(now|uuid|cuid)$/).test(token)) color = colors.name; else if ((/^(String|Int|Float|Boolean|DateTime|Json)$/).test(token)) color = colors.type; else if (leading && match.index === leading[1].length) color = colors.variable; else if (bare && match.index === bare[1].length) color = colors.constant; else if ((/^[A-Z]/).test(token)) color = colors.name; else if ((/^[A-Za-z_]/).test(token)) color = colors.variable;
            append(line, token, color);
            offset = match.index + token.length;
          }
          append(line, codeText.slice(offset));
          if (start !== -1) append(line, text.slice(start), colors.comment);
        }
        fragment.append(line, '\n');
      }
      code.replaceChildren(fragment);
    };
    const highlightRepostBlocks = () => {
      for (const block of document.querySelectorAll('.code-block')) {
        const code = block.querySelector('pre code');
        const filename = block.querySelector('[data-component-part="code-block-header-filename"] [title$=".repost"]');
        const looksLikeRepost = (/^\s*(generator|model|enum|type|event)\s+[A-Za-z_]\w*\s*\{/m).test(code?.textContent ?? '');
        if (code && (filename || looksLikeRepost)) highlightCode(code);
      }
    };
    let frame;
    const schedule = () => {
      if (frame !== undefined) return;
      frame = requestAnimationFrame(() => {
        frame = undefined;
        highlightRepostBlocks();
      });
    };
    const observer = new MutationObserver(schedule);
    observer.observe(document.documentElement, {
      childList: true,
      subtree: true
    });
    schedule();
    return () => {
      observer.disconnect();
      if (frame !== undefined) cancelAnimationFrame(frame);
    };
  }, []);
  return null;
};

<RepostHighlight />

Events are defined in a `.repost` schema file. It describes the shape of each payload, the name of each event, and which SDKs to generate. The generated client, the payloads on the wire, and the event docs your customers read all come from this file, so they stay in sync without any effort on your part.

The syntax follows Prisma's schema language. A complete schema looks like this:

```repost repost/schema.repost theme={"languages":{"custom":["/languages/repost.json"]}}
generator sdk {
  language = "typescript"
}

enum Currency {
  USD
  EUR
}

type Book {
  /// A book was created
  created
  updated
  deleted
}

model Author {
  name  String
  email String?
}

model Book {
  title    String
  authors  Author[]
  price    Float
  currency Currency
  subtitle String?
}

model DeletedBook {
  id String
}

event BookCreated {
  type      @type(Book.created)
  data      Book
  timestamp DateTime
}

event BookUpdated {
  type      @type(Book.updated)
  data      Book
  timestamp DateTime
}

event BookDeleted {
  type      @type(Book.deleted)
  data      DeletedBook
  timestamp DateTime
}
```

There are four kinds of block. A `model` describes a payload, a `type` lists your event names, an `event` ties a name to a payload, and a `generator` selects an SDK to emit.

## Where a schema line ends up

Consider the line `price Float`. After generation it appears in three places.

In your code, it becomes a typed field. This is the actual generated TypeScript:

```ts generated index.d.ts theme={"languages":{"custom":["/languages/repost.json"]}}
export interface Book {
  title: string;
  authors: Author[];
  price: number;
  currency: Currency;
  subtitle?: string | null;
}

export interface Webhooks {
  book: {
    /** A book was created */
    created(input: { customerId: string; data: Book; idempotencyKey?: string }): Promise<SendResult>;
    updated(input: { customerId: string; data: Book; idempotencyKey?: string }): Promise<SendResult>;
    deleted(input: { customerId: string; data: DeletedBook; idempotencyKey?: string }): Promise<SendResult>;
  };
}
```

Each catalog member became a method, so `Book.created` is called as `webhooks.book.created(...)`, and the `///` comment above the member became the method's documentation. Passing a string as `price` is a compile error.

On the wire, it becomes a payload field:

```json theme={"languages":{"custom":["/languages/repost.json"]}}
{
  "type": "book.created",
  "timestamp": "2026-07-17T12:00:00.000Z",
  "data": {
    "title": "The Pragmatic Programmer",
    "authors": [{ "name": "Andy Hunt" }],
    "price": 42.9,
    "currency": "USD"
  }
}
```

Fields serialize in declaration order in every language. The wire name of the event is the lowercased catalog name, a dot, and the member: `book.created`.

In your customers' hands, it becomes documentation. Deploying the schema publishes `book.created` to your environment with a JSON Schema of the payload and the doc comment as its description. The [portal's event catalog](/docs/send/portal) and your [public event docs](/docs/send/event-docs) render that registry directly.

## Models

A `model` is a list of typed fields:

```repost theme={"languages":{"custom":["/languages/repost.json"]}}
model Order {
  id        String    @default(cuid())
  reference String    @map("order_reference")
  amount    Float
  express   Boolean   @default(false)
  lines     Line[]
  coupon    String?
  metadata  Json?
  createdAt DateTime  @default(now())
}

model Line {
  sku String
  qty Int    @default(1)
}
```

### Field types

| Type          | Generated (TypeScript) | On the wire                          |
| ------------- | ---------------------- | ------------------------------------ |
| `String`      | `string`               | JSON string                          |
| `Int`         | `number`               | JSON number                          |
| `Float`       | `number`               | JSON number                          |
| `Boolean`     | `boolean`              | JSON boolean                         |
| `DateTime`    | `string`               | ISO-8601, millisecond precision, UTC |
| `Json`        | JSON value             | Passed through as-is                 |
| Another model | Its interface          | Nested object, same rules            |
| An enum       | Its type               | The member's wire value              |

Modifiers compose. `String?` is optional, `Line[]` is a list, and `String[]?` is an optional list.

### Absent is not null

Optional fields have three states, and the SDKs keep them separate. A field that is set serializes its value. A field that is absent is omitted from the payload, unless it declares a `@default`, in which case the default is filled in. An explicit null serializes as `null`. Consumers can rely on the difference between a missing field and a null one.

### Defaults

`@default` fills a field in when you don't provide a value. It accepts literals (`@default("fallback")`, `@default(1)`, `@default(false)`, `@default(USD)`), the send time (`@default(now())`), and generated ids (`@default(uuid())`, `@default(cuid())`).

### Wire names

Schema names are for your code. `@map` sets the name used in the payload:

```repost theme={"languages":{"custom":["/languages/repost.json"]}}
model Order {
  reference String @map("order_reference")
}
```

Your code reads `order.reference`; the payload contains `order_reference`. Enum members can be renamed the same way: `RED @map("scarlet")`.

## Enums

```repost theme={"languages":{"custom":["/languages/repost.json"]}}
enum Currency {
  USD
  EUR
}
```

An enum generates a value object and a type (`Currency.USD` in TypeScript) and serializes as the member name, or the `@map` value if the member was renamed.

## Event types

The `type` block names your events. Each `event` block gives one of them a payload:

```repost theme={"languages":{"custom":["/languages/repost.json"]}}
type Book {
  /// A book was created
  created
  deleted
}

event BookCreated {
  type      @type(Book.created)
  data      Book
  timestamp DateTime
}

event BookDeleted {
  type      @type(Book.deleted)
  data      DeletedBook
  timestamp DateTime
}
```

Events in the same catalog can carry different payloads. A deletion usually only needs an id, so it gets a smaller model.

The `///` comment on a member is worth writing. It becomes the generated method's documentation, the event's description in your environment, and the description shown in the portal catalog and your public event docs.

A schema can hold any number of catalogs. `type Order`, `type Invoice`, and `type ApiKey` each become their own branch of the method tree, such as `apiKey.rotated`.

## Generators

A `generator` block emits one SDK. A schema can carry several, so a monorepo can generate every service's client from the same file:

```repost theme={"languages":{"custom":["/languages/repost.json"]}}
generator api {
  language = "typescript"
}

generator payments {
  language = "go"
  output   = "../services/payments/internal/repostclient"
}
```

Supported languages are `typescript`, `go`, `python`, `java`, and `kotlin`. TypeScript writes into `node_modules` by default. The other languages commit their generated package to the repo and require an `output` path. Java and Kotlin take a few extra settings for their build plugins, covered in the [Java](/docs/send/java/generation) and [Kotlin](/docs/send/kotlin/generation) guides.

## Validating and formatting

`repost schema validate` checks the schema without writing anything. `repost schema fmt` formats it in place. Both exit non-zero on problems, so they work as CI checks. The schema lives in `repost/schema.repost`, or in a `repost/schema/` directory of `.repost` files once it outgrows a single file; use one layout or the other, not both.

Changing the schema is a versioned operation. The next page covers how changes are recorded and deployed.

## Continue

<Columns cols={3} className="gap-y-4">
  <Card title="Migrations & deploys" icon="git-branch" href="/docs/send/deployments" cta="Version it" arrow="true">
    Record schema changes as migrations and put them live.
  </Card>

  <Card title="SDKs" icon="package" href="/docs/send/sdks" cta="Generate" arrow="true">
    What these definitions become in each language.
  </Card>

  <Card title="Event docs" icon="book-open" href="/docs/send/event-docs" cta="Docs" arrow="true">
    The customer-facing pages generated from your doc comments.
  </Card>
</Columns>
