URL: /code/apps/mailboxes/schedule · Id: `page:/code/apps/mailboxes/schedule`

# Message.schedule

> Hold one email for a set time, such as 9:10 in the buyer’s morning.

`POST https://api.sb/{your-domain}/messages/schedule`

Hold one email for a set time, such as 9:10 in the buyer’s morning.

- **Action class:** None
- **Approval:** Runs on its own
- **Emits:** `Message.scheduled`
- **Sites:** Gmail, LinkedIn, Outlook, X
- **Price:** Free

```text title="Run it · Ask"
Send Rosa at Northgate Rentals my follow-up at 9:10 a.m. her time on Thursday.
```

```ts title="Run it · SDK"
import { $ } from 'startup-builder'

const result = await $.try.Message.schedule({ message: 'msg_Fu3pK7xR1d', at: '2026-10-29T14:10:00Z' })
```

```json title="Run it · Response"
{
  "api": "https://api.sb/leaseline.app",
  "message": {
    "$id": "https://api.sb/leaseline.app/messages/msg_Fu3pK7xR1d",
    "$type": "Message",
    "medium": "email",
    "direction": "outbound",
    "from": "dana@tryleaseline.app",
    "to": "rosa@northgate-rentals.com",
    "subject": "Re: the leasing coordinator role",
    "purpose": "sales"
  },
  "at": "2026-10-29T14:10:00Z",
  "action": {
    "operation": "Message.schedule",
    "mode": "try",
    "status": "done"
  }
}
```

Holds one email until the minute you set, such as 9:10 in the buyer’s morning, then sends it from your Mailbox. [The hub’s definition of `Message.schedule`](https://startupbuilder.dev/business/nouns/message/schedule).</HubDefinition>

## Inputs
| Name | Type | Default | Meaning |
| --- | --- | --- | --- |
| `message` | `Message` | Required | The Message to hold. |
| `at` | `datetime` | Required | When it goes out. |

## Outputs
| Field | Type | What `null` means |
| --- | --- | --- |
| `message` | `Message` | Never null. |
| `message.from` | `string` | Never null: the Mailbox address it sends from. |
| `message.to` | `string` | Never null for email: the one recipient. |
| `message.subject` | `string` | Null for a medium with no subject line, such as SMS. |
| `message.purpose` | `string` | Never null: the Consent and Suppression checks read it before it sends. |

## Effects
**Action class**

None: no approval rule names it, so it runs within its seat’s Permissions and Limits

**Modes**

- `try`: Runs the send checks against `at` and renders the Message; nothing is held or sent.
- `send`: Returns the held Message at once; it sends at `at`.
- `do`: Returns the held Message at once; it sends at `at`.

**Idempotency**

Only with a key: the same `Idempotency-Key` and input return the first Action, and the same key with other input returns `409`. Scheduling a held Message again moves it to the new `at`; it never sends twice.

**Saved or shared**

The Message is saved on its Contact and held as a pending Action, `reason: 'awaitingTime'`, until `at`. Canceling that Action before then stops it.

**Emits**

`Message.scheduled` with `{ message, at }`

**Performed by**

[Marketing lead](https://startupbuilder.dev/agents/marketing/marketing-lead)

**Price**

Free on your own account: the platform charges nothing for the call.

## On each Venue
| Site | Fields | Formats |
| --- | --- | --- |
| [Gmail](#gmail) | `from`, `to`, `subject`, `body` | None. |
| [LinkedIn](#linkedin) | `body` | 3 |
| [Outlook](#outlook) | `to`, `subject`, `body` | None. |
| [X](#x) | `body` | 2 |

### Gmail
Through [Gmail](/code/integrations/gmail).

| Our field | Gmail’s field |
| --- | --- |
| `Message.from` | `raw: From header` (Your address, or a send-as alias you set up in Gmail) |
| `Message.to` | `raw: To header` |
| `Message.subject` | `raw: Subject header` (A reply keeps the thread’s subject, so Gmail threads it) |
| `Message.body` | `raw: text/plain and text/html parts` |

- **Replies.** Gmail pushes each new message within seconds; the platform renews the mailbox watch daily, inside the 7 days Gmail allows.

### LinkedIn
Through [LinkedIn](/code/integrations/linkedin).

| Our field | LinkedIn’s field |
| --- | --- |
| `Message.body` | `invitation note or message` (Typed into LinkedIn’s own screens by the browser agent) |

- **Connection requests.** Capped by your standing approval at 20 a weekday, about 100 a week; LinkedIn publishes no number, and a request past your standing approval waits with `over-mandate`.
- **Pending requests.** A request unanswered after 3 weeks is withdrawn, so pending requests never pile up.
- **Replies.** Read from your LinkedIn inbox every 30 minutes on weekdays, since LinkedIn sends no webhook for member messages.

Feed post Single image ad, 1200 × 628, 1200 × 1200 or 720 × 900 Document ad

### Outlook
Through [Outlook](/code/integrations/outlook).

| Our field | Outlook’s field |
| --- | --- |
| `Message.to` | `message.toRecipients` |
| `Message.subject` | `message.subject` |
| `Message.body` | `message.body` (contentType html or text) |

### X
Through [X account](/code/integrations/x).

| Our field | X account’s field |
| --- | --- |
| `Message.body` | `text` |

- **Each direct message.** X bills 1.5¢.
- **Replies to strangers.** Only to posts that describe your problem, and never twice in one thread without an answer back.

Standard post Promoted post

## What try does
| Account | What try does |
| --- | --- |
| [Gmail](/code/integrations/gmail#try) | Renders the exact email, headers included, and returns it with the time it would send; nothing reaches Gmail and no draft is saved. |
| [LinkedIn](/code/integrations/linkedin#try) | Posts render as LinkedIn will show them and nothing is sent; requests and messages return each person, the note and the day it goes, and no browser session opens. |
| [Outlook](/code/integrations/outlook#try) | Renders the exact email or invite and returns it with the time it would go; nothing reaches Outlook and no draft or event is saved. |
| [X account](/code/integrations/x#try) | Renders each post at X’s weighted count, with links at 23 characters, and returns the price X would bill; nothing is posted and nothing is billed. |

## Refuses when
| Check | Error | Fix | Then call |
| --- | --- | --- | --- |
| A first cold email from a Mailbox that has not finished warming | [`mailbox-warm`](/code/refusals#check-mailbox-warm) | Wait for the warm-up to finish, 14 days from day 0; warm intros keep running. | [`Mailbox.warm`](https://startupbuilder.dev/business/nouns/mailbox/warm) |
| A send from a Domain whose DNS records do not resolve | [`domain-authenticated`](/code/refusals#check-domain-authenticated) | Add the 3 DNS records shown; it passes once they resolve, often within an hour. | [`Domain.authenticate`](https://startupbuilder.dev/business/nouns/domain/authenticate) |

Every call through an account can also meet [the shared account refusals](/code/refusals#accounts): [`grant-missing`](/code/refusals#grant-missing), [`needs-reauth`](/code/refusals#needs-reauth), [`account-not-connected`](/code/refusals#account-not-connected), [`over-mandate`](/code/refusals#over-mandate), [`quota-reached`](/code/refusals#quota-reached), [`venue-rejected`](/code/refusals#venue-rejected).

```json
{
  "type": "https://startupbuilder.dev/errors/mailbox-warm",
  "title": "Inbox not warmed up",
  "status": 422,
  "detail": "A first cold email from a Mailbox that has not finished warming.",
  "fix": "Wait for the warm-up to finish, 14 days from day 0; warm intros keep running.",
  "next": {
    "verb": "Mailbox.warm",
    "input": {}
  },
  "instance": "https://api.sb/leaseline.app/actions/action_1"
}
```

## When it waits
Returns in one response; nothing waits.

## Example

**Ask**

```text
Send Rosa at Northgate Rentals my follow-up at 9:10 a.m. her time on
Thursday.
```

**API**

```bash
curl -X POST "https://api.sb/leaseline.app/messages/schedule?mode=try" \
  -H "Authorization: Bearer $SB_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"message":"msg_Fu3pK7xR1d","at":"2026-10-29T14:10:00Z"}'
```

The response is the one in the Run it window above.

**CLI**

```bash
npx startup-builder do Message.schedule --try --data '{"message":"msg_Fu3pK7xR1d","at":"2026-10-29T14:10:00Z"}'
```

**MCP**

```json
{
  "name": "do",
  "arguments": {
    "mode": "try",
    "code": "return await $.Message.schedule({ message: 'msg_Fu3pK7xR1d', at: '2026-10-29T14:10:00Z' })"
  }
}
```

**SDK**

```ts
import { $ } from 'startup-builder'

const result = await $.try.Message.schedule({
  message: 'msg_Fu3pK7xR1d',
  at: '2026-10-29T14:10:00Z',
})
```

To retry, send the same `Idempotency-Key`: the retry returns the first answer for $0. A new key reads again.

A `sk_test_` key answers these the same way every time, at $0 ([every test value](/code/data#test-values)).

| Test value | Always returns | What it is for |
| --- | --- | --- |
| `msg_test_unwarmed` | Refuses with [`mailbox-warm`](/code/refusals#check-mailbox-warm) | Its Mailbox is on day 6 of a 14-day warm-up, so the first cold email holds until the warm-up ends. |
| `maxCost: 0.0001` | Refuses with [`max-cost`](/code/refusals#max-cost) | Any priced call with a maxCost of 0.0001 is refused before it spends: the price is above the cap. |
| `rate-limited.test` | Refuses with [`rate-limited`](/code/refusals#rate-limited) | Any call naming this hostname answers 429 with Retry-After, so your retry logic runs. |
| `sources-busy.test` | Refuses with [`sources-busy`](/code/refusals#sources-busy) | Any call naming this hostname answers 503 as if every route were busy, and nothing is spent. |

## Used by
- **Workflows:** [Review requests](/capabilities/keep-and-grow/review-requests)
- **Agent tasks:** [Referral ask](/capabilities/keep-and-grow/referral-ask) · [Link pitches](/capabilities/earn-first-links/link-pitches) · [Upgrade note at the limit](/capabilities/keep-and-grow/upgrade-note) · [Save and win back](/capabilities/keep-and-grow/save-and-win-back)
- **Jobs:** [Earn first links](/capabilities/earn-first-links) · [Keep and grow customers](/capabilities/keep-and-grow) · [Reach out](/capabilities/reach-out)
- **Integrations:** [Gmail](/code/integrations/gmail) · [LinkedIn](/code/integrations/linkedin) · [Outlook](/code/integrations/outlook) · [X account](/code/integrations/x)

## Related
[`Message.broadcast`](/code/apps/mailboxes/broadcast)