---
title: "Schedule and cancel"
description: "`scheduledAt`, `emails.reschedule`, `emails.update` and `emails.cancel`."
url: "https://openemail.uk/docs/ruby/emails/schedule"
area: "Ruby"
category: "Emails"
---

# Schedule and cancel

`scheduledAt`, `emails.reschedule`, `emails.update` and `emails.cancel`.

## Sending later

**schedule.rb**

```
message = {from: "billing@acme.com", to: "ada@example.com", subject: "Your September invoice", text: "Invoice attached."}

client.emails.send(message, scheduledAt: "PT1H")
client.emails.send(message, scheduledAt: Time.utc(2027, 1, 1, 9))
client.emails.send(message, scheduledAt: "2027-01-01T09:00:00.000Z")
```

A Time or a DateTime, an ISO 8601 instant as a String, or a duration like `PT1H` or `P2D`. Up to a year out, never in the past. A keyword argument beside the Hash adds the field to a message you built earlier.

> A Ruby Date is sent as a bare date such as `2027-01-01`, which the API reads as midnight UTC on that day. Pass a Time, such as `Time.utc(2027, 1, 1, 9)`, when the hour matters.

## Moving and stopping

**reschedule.rb**

```
message = {from: "billing@acme.com", to: "ada@example.com", subject: "Your September invoice", text: "Invoice attached."}

queued = client.emails.send(message, scheduledAt: "PT1H")

client.emails.reschedule(queued[:id], Time.now + 86_400)
client.emails.cancel(queued[:id])
```

Only `queued` and `scheduled` messages can be stopped. Anything further on raises an `OpenEmail::ConflictError`, because some of it is already in somebody’s mailbox. Cancelling an already-cancelled message succeeds and changes nothing.

To find what is waiting to go out in a window, list with `status: ["scheduled", "queued"]` and `scheduled_from:` and `scheduled_to:`, as the calendar of the app does.

## Changing it before it goes

`emails.update` changes a message that has not gone yet: when it goes with `scheduledAt`, what it says with `subject`, `html` and `text`, the address it goes out as with `from`, and who it goes to with `to`, `cc` and `bcc`. Send any of them together, and a field you leave out keeps its value. A recipient list replaces the stored one whole. It is what editing a scheduled message in the calendar of the app does.

**update.rb**

```
updated = client.emails.update(
  "msg_3f9a1c07d2b84e6a9c5b1f20",
  subject: "Your September invoice, corrected",
  to: ["ada@example.com", "grace@example.com"],
  scheduledAt: Time.utc(2026, 10, 5, 8)
)

puts updated[:status], updated[:subject], updated[:scheduledAt]
```

> `from` is checked as it is on a send, so it has to be an address the key may send as. A message translated when it was accepted keeps its approved wording, so a new `subject`, `html` or `text` on it is a 409 `translation_locked`, and one that was encrypted before it was scheduled keeps its wording and its recipients. Cancel those and send again instead.

## An undo window instead

**undo_window.rb**

```
message = {from: "billing@acme.com", to: "ada@example.com", subject: "Your September invoice", text: "Invoice attached."}

held = client.emails.send(message, cancellableForSeconds: 30)

puts held[:status], held[:cancellableUntil]
```

> A scheduled message is already cancellable until it goes, so the two cannot be combined, and the server refuses it. Use this one for an undo-send window on an immediate message.

## Parameters: scheduling

- `scheduledAt` (Time, DateTime or String): When to send, on `emails.send`: a Time or a DateTime, an ISO 8601 instant as a String, or a duration like `PT1H` or `P2D`. A Time or a DateTime is sent as a UTC instant, a String as it is, and a Ruby Date as a bare date that means midnight UTC. At least one second in the future and at most 365 days out, with either bound a `validation_error` on `scheduledAt`, and natural language is not accepted, because parsing "next Tuesday" wrongly sends a message at a time that cannot be taken back.
- `cancellableForSeconds` (Integer): An undo-send window on an IMMEDIATE send: an Integer from 0 to 900, defaulting to 0. Any value above 0 is refused alongside `scheduledAt`, which is already cancellable until it goes, and a message held this way sits at `queued` rather than `scheduled`. It is the same deferral mechanism with a short delay.
- `id` (String, required): The `msg_` id, and the first argument to `emails.cancel`, `emails.reschedule` and `emails.update`. They need `emails:send` rather than a scope of their own, and they look the id up within the key’s own workspace, so an id belonging to another one is a `not_found_error` exactly like an id that never existed.
- `scheduled_at` (Time, DateTime or String, required): The new time, as the second argument to `emails.reschedule`, read by the same rules and against the same one-year window. It is the only thing `reschedule` changes, and the client sends nothing else. A duration is relative to when the SERVER reads it, so a retried reschedule lands slightly later than the first would have: later, never earlier.
- `api_key` (String): Acts with this key instead of the client’s, on any of the three calls.

## Response

`cancel`, `reschedule` and `update` each return the whole message as a Hash with Symbol keys.

- `object` (String): Always `email`. These calls answer with the whole message rather than an acknowledgement, so nothing has to be fetched again to see what changed. `emails.send` returns this same shape plus `replayed`.
- `id` (String): The `msg_` handle. Stable for the life of the message and the id every other call on it takes.
- `status` (String): `cancelled` after a cancel and `scheduled` after a reschedule, including for a message that was only `queued` behind an undo window, which a reschedule turns into a real schedule. Only `queued` and `scheduled` messages can be moved or stopped. Anything further on is a `conflict_error` with code `email_not_cancellable`, because some of it is already in somebody’s mailbox.
- `scheduledAt` (String or nil): The ISO 8601 instant the message is due to be dispatched. Set for an undo window as well as for a `scheduledAt` send, since the two are one mechanism, and nil on a plain immediate send.
- `cancellableUntil` (String or nil): When cancelling stops working, which is the same instant as `scheduledAt` on both deferred paths. nil on an immediate send, which has already gone by the time the call returns.
- `sentAt` (String or nil): When the message actually left. nil while it waits, and nil for ever on a cancelled one.
- `messageId` (String or nil): The RFC 5322 Message-ID, nil until the MIME exists, so always nil on a message these calls can act on. Not something to address the API with, and not what a later bounce comes back on either: the sending service rewrites the header on the way out.
- `threadId` (String or nil): The thread this message belongs to, taken from the request and rewritten with whatever the transport reports once it sends. nil when it is not a reply.
- `transport` (String or nil): How the bytes left, and nil until dispatch, so nil on every message a cancel or a reschedule can return. A test-mode send records `test`, and a transport this gem does not name yet may appear, so treat an unknown value as information rather than an error.
- `attempts` (Integer): How many times dispatch has claimed this row. It goes up with each claim rather than with a successful send, and is 0 for anything still waiting.
- `lastError` (String or nil): The last failure recorded against the message, nil while nothing has failed. A deferred send whose job could not be queued is written here as `Could not schedule: …` and moved to `failed`, which is the one way a scheduled message stops being cancellable without anybody asking.
- `from` (String): The address the message was authorised to go out as: the `from` that was sent, stored bare and lower-cased. Any display name is dropped here, because the `from:` filter of `emails.list` compares on equality.
