---
title: Deadlines and timers
description: "Schedule a typed event back to the same instance. Deadlines survive deploys and land on the lane like any other message."
---

## A deadline is a message, delivered later

```ts escrow.ts
import { actor } from 'experimental-a2/actor'
import { vercelQueues } from 'experimental-a2/scheduler-vercel'

interface Escrow {
  state: { held: Record<string, number> }
  events: {
    hold: { offerId: string; amount: number }
    accept: { offerId: string }
    release: { offerId: string }
  }
}

export const escrow = actor<Escrow>({
  name: 'escrow',
  state: { held: {} },
  scheduler: vercelQueues(),
  handlers: {
    hold: async (ctx, input) => {
      ctx.state.held[input.offerId] = input.amount
      await ctx.schedule.release({ offerId: input.offerId }, { delay: '1h' })
    },
    accept: (ctx, input) => {
      if (ctx.state.held[input.offerId] === undefined) return
      delete ctx.state.held[input.offerId]
      // your payout; ctx.id is the idempotency key
    },
    release: (ctx, input) => {
      if (ctx.state.held[input.offerId] === undefined) return
      delete ctx.state.held[input.offerId]
      // your refund; ctx.id is the idempotency key
    },
  },
})
```

`ctx.schedule` mirrors `ctx.send`, one method per declared event, with
a timing argument: exactly one of `{ delay: '1h' }` or `{ at: date }`.
It requires the `scheduler` option. The deadline outlives the
invocation that armed it and any deploys in between, and delivery lands
on the instance's lane like any other message.

Adapters, delay limits, and delivery semantics are the
[session timers](/server/timers) underneath.

## Stale timers are no-ops

When `accept` wins, the release timer still fires an hour later, meets
the `held[offerId] === undefined` guard, and does nothing. When the
timer wins, a late `accept` meets the same guard. There is no
cancellation API, and the log records that the timer fired: let the
event land and return when it no longer applies.

## Identity and retries

A timer's name defaults to the target event's name, scoped to the
triggering message, so a re-run of a crashed handler addresses the same
timer instead of arming a second. One handler run gets one timer per
target event; to arm several toward the same event, name them apart:

```ts
// inside a handler:
await ctx.schedule.release(
  { offerId: 'offer-42' },
  { delay: '30m', name: 'first-nudge' },
)
```

Duplicate deliveries fold into one append, and relative delays anchor
to the triggering message's `createdAt`; see
[Identity and retries](/server/timers#identity-and-retries).

## Scheduling is immediate

:::warning
Unlike `ctx.send`, `ctx.schedule` is not buffered: a timer lives at
the provider, not in the log, so the call awaits provider acceptance
right where it happens. A handler that schedules and then refuses has
still armed the timer. Guarded delivery makes that safe, but order
handlers accordingly: guards and refusals first, `ctx.schedule` last.
:::

## From outside a handler

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

await escrow.actor('offer-42').session.schedule(
  'expire-offer',
  { at: new Date('2030-01-01T00:00:00Z') },
  { type: 'release', payload: { offerId: 'offer-42' } },
)
```

Outside handler contexts, schedule through the instance's session with
an explicit name, timing, and event. Root scheduling semantics apply
(`{ at }` for retry-stable targets); see
[Schedule from ordinary server code](/server/timers#schedule-from-ordinary-server-code).
