---
title: Introduction
description: "Typed state behind a mailbox. Each instance processes one message at a time, and each handler ends in one atomic commit."
---

## The model

An actor is state behind a mailbox: you send an instance typed events,
and its handlers process them one at a time. Between messages an
instance is rows in your store, not a process. Every instance is an
ordinary A2 session, so durability, retries, history, and devtools hold
for actors too.

Entry points: `experimental-a2/actor` on the server,
`experimental-a2/actor/client` anywhere, and
`experimental-a2/actor/react` for the hook.

## Define one

```ts vault.ts
import { NonRetriableError } from 'experimental-a2'
import { actor } from 'experimental-a2/actor'

interface Vault {
  state: { balance: number }
  events: {
    deposit: { amount: number }
    withdraw: { amount: number }
  }
}

export const vault = actor<Vault>({
  name: 'vault',
  state: { balance: 0 },
  handlers: {
    deposit: (ctx, input) => {
      ctx.state.balance += input.amount
    },
    withdraw: (ctx, input) => {
      if (ctx.state.balance < input.amount) {
        throw new NonRetriableError('insufficient funds')
      }
      ctx.state.balance -= input.amount
    },
  },
})
```

Pass the protocol explicitly as the type argument. It declares types,
not schemas: handlers are trusted server code, and a missing handler,
an extra one, or a wrong input shape fails to compile. `ctx.state` is a
mutable draft, committed atomically when the handler returns. `name` is
identity (it prefixes storage keys), and `state` seeds new instances.

When an event can arrive from the wire, guard its input in the
handler's first lines and throw `NonRetriableError` on bad shapes.

`actor` also accepts `createServer`'s `store`, `scheduler`, and
`telemetry` options, with the same development default store; see
[Stores](/stores/overview) and [Schedulers](/schedulers/overview). A protocol can also declare
[`presence`](/actors/routes#presence).

## Call it

```ts
// anywhere on the server:
import { vault } from '@/vault'

const savings = vault.actor('savings')

const { state, index } = await savings.call.deposit({ amount: 100 })
// state reflects this deposit, and every message before it
```

`call` awaits the answer: the state after the handler ran and the log
index of that commit. `vault.actor(id)` is a handle; nothing exists
until the first message. Concurrent calls from any number of
invocations serialize on the instance's lane, and each answer reflects
its own message; see [Concurrency](/actors/concurrency).

### Refusals

```ts
// anywhere on the server:
import { ActorRefusedError } from 'experimental-a2/actor'
import { vault } from '@/vault'

try {
  await vault.actor('savings').call.withdraw({ amount: 1_000_000 })
} catch (error) {
  if (error instanceof ActorRefusedError) {
    // the answer was no: state unchanged, message settled
  }
}
```

Throwing `NonRetriableError` refuses the message. A refusal is an
answer, and it is all or nothing: no state change, no buffered sends,
no retry. The message settles, and the caller receives
`ActorRefusedError`.

- **Any other throw** retries the message with the usual budget and
  backoff while the instance's lane waits; see
  [Errors](/reference/errors).
- **A crash** re-runs the handler from the pre-message state. `ctx.id`
  is the message id, stable across re-runs: use it as the idempotency
  key for external I/O.
- **A clear** by `handle.session.clear()` before the answer rejects the
  call with an `A2Error` whose code is `SESSION_CLEARED`. The handler
  may already have run. Over HTTP it's a `410`.

## Send, don't wait

```ts
// anywhere on the server:
import { vault } from '@/vault'

const { id } = await vault.actor('savings').send.deposit({ amount: 25 })
// durably queued; the handler runs on the instance's lane
```

`send` validates and appends the event durably without waiting for the
handler: same mailbox, same ordering. Pass `{ id }` in the options to
make a retried send idempotent.

## Read without queueing

```ts
// anywhere on the server:
import { vault } from '@/vault'

const { state } = await vault.actor('savings').state()
```

A snapshot read: it never queues behind pending messages, and a new
instance reads as its seed state.

## State is remembered, not recomputed

Every commit appends a state event, and reads fold them by last write
wins. Unlike [reducers](/contract/state), nothing is recomputed, so
rewriting a handler never changes what a past commit means. State must
be plain JSON: a `Date`, a `Map`, or `NaN` fails the commit loudly.

- **Old instances meet new code.** After a deploy, default new fields
  defensively (`ctx.state.pending ??= {}` as a first line).
- **The seed applies to new instances only.** Changing `state` does not
  migrate existing instances.

## The log is still there

`handle.session` is the instance's underlying session (history,
streams, scheduling). The definition exposes its assembled `contract`,
`server`, and `reducer`, and devtools show the full mailbox. Event
names starting with `a2.` are reserved for the actor's own bookkeeping.
