Threads, drafts and labels
Every command in the threads, drafts and labels namespaces, and how they sit under inbox, read, archive and the other mail commands.
Overview
The mail commands, such as inbox, read, archive and label add, are written for people: they take several thread ids at once, format what they print and keep the label ids out of sight. Each of them runs commands from this page, which are the SDK methods for threads, drafts and labels, one command per method, so threads.listAttachments is openemail threads list-attachments.
Use these when you need what the mail commands leave out: a thread exactly as the API returns it, the files on a message, drafts, and making, renaming, recolouring or deleting labels.
openemail threadandopenemail draftwork as well as the plural names.openemail labelshas no singular form:openemail labelis the mail command that puts labels on threads.- The verbs take the usual aliases:
lsforlist,showandviewforget,newandaddforcreate,editforupdate, andrm,delandremovefordelete. - Every flag is in
openemail <namespace> <verb> --help, such asopenemail threads list --help.
Threads
Conversations in the mailbox. A thread id such as CAHk7pQ2x9LmZ4 comes from threads list, openemail inbox or openemail search.
| Command | What it does |
|---|---|
| openemail threads list | List one page of threads in a folder, newest first. Each row is only an id. --folder, --query, --label-ids, --sort, --date-from, --date-to and --from-contacts narrow and order it |
| openemail threads get <id> | Read a thread with every message on it, oldest first, with its labels and unread state |
| openemail threads update <id> | Mark a thread read with --read or unread with --no-read, and put labels on or take them off with --add-label-ids and --remove-label-ids, up to 50 each |
| openemail threads trash <id> | Move a thread to the Bin, out of the inbox, spam, snoozed and archive in one step. Asks you to confirm |
| openemail threads snooze <id> <wake-at> | Hide a thread until a future instant, such as 2026-10-01T09:00:00Z. Snoozing it again replaces the wake time |
| openemail threads unsnooze <id> | Bring a snoozed thread back to the inbox now and clear its wake time |
| openemail threads list-attachments <id> <message-id> | List one message's attachments, each with its bytes inline as base64 in content |
--folderdefaults toinboxand is matched as a label id, sosent,archive,spam,trash,draft,snoozed,starredandunreadwork,binreads astrash, and a user label id such asUSER_RECEIPTSworks too. A folder that matches nothing returns an empty page, not an error.--querytakes the search syntax of the app, andin:anywheresearches every folder.--label-idsnarrows further, since a thread must carry the folder and every id you pass.--date-fromand--date-toread the newest message on each thread, and both ends are included.threads getincludes unsent draft replies among the messages, markedisDraft: true, and it opens a draft id too.threads updateneeds--read,--no-reador a label to add or remove. Removals are applied before additions. A label id that names no label is refused withlabel_not_foundand nothing on the thread changes, so create the label first.TRASH,SNOOZEDandDRAFTare refused withlabel_not_directly_settable: usethreads trashandthreads snooze.threads trashdeletes nothing, and the thread stays readable withthreads get, but no command takes a thread back out of the Bin. Trashing a snoozed thread also cancels its wake.threads snoozesends<wake-at>as it is, so give it a future ISO 8601 instant withZor an offset, since a time without one is read in the time zone of the server. A delay such as3his refused as invalid.openemail snooze --until 3htakes a delay. Threads wake at an hourly sweep, up to about an hour late, and always into the inbox.threads list-attachmentsreturns every file whole in one response. Take the message id from themessagesofthreads get.contentis an empty string when the stored bytes cannot be found, so check its length before decoding.
Drafts
Unsent messages saved in the mailbox. A draft id starts with draft-.
| Command | What it does |
|---|---|
| openemail drafts list | List one page of drafts, most recently saved first. Each row is only an id, and --query searches them |
| openemail drafts get <id> | Read a draft's recipients, subject, body, sender, the thread it replies to and the names of its attachments |
| openemail drafts create | Save a new draft from --to, --cc, --bcc, --subject, --html, --text, --from and --thread-id, all optional |
| openemail drafts update <id> | Change fields on a saved draft. A field you leave out keeps its value |
| openemail drafts delete <id> | Delete a draft for good. It does not go to the Bin. Asks you to confirm |
drafts list --querysearches the subject, the sender and the start of the body, and never leaves drafts.older_than:30dand the other date operators read when the draft was last saved, andto:,cc:andbcc:match nothing on a draft.- A draft is stored as a thread labelled
DRAFT, sothreads getopens one andopenemail inbox draftlists them.drafts get,updateanddeleterefuse an ordinary thread id with a 404. - A bare
openemail drafts createsaves a blank draft. Only lengths are checked: a subject up to 998 characters, and--htmland--textup to 1,000,000 each, with--htmlkept when both are set. There is no flag for attachments. drafts updatereplaces each field you send. A list replaces the stored one whole, so--towith one address drops the others, and an update empties the draft’s attachment list.--thread-idrecords the thread a draft replies to, but the draft is still stored as a thread of its own.- Running
drafts createagain saves a second draft, because it takes no idempotency key. A display name with a comma in it splits into two broken recipients, so leave the comma out. openemail send --draft <id> --to <address>sends a draft. The body comes from the draft, and so does the subject unless you pass--subject, while the recipients are the ones you name. It cannot be combined with a body,--templateor--translate.
Labels
The labels a thread can carry. A user label id is USER_ followed by the name it was created with, upper cased, with each run of whitespace turned into _, so Big Clients is USER_BIG_CLIENTS.
| Command | What it does |
|---|---|
| openemail labels list | List the workspace's user labels, sorted by name, each with its colour, threadCount, createdAt and updatedAt |
| openemail labels list-colors | List the palette the app offers, fourteen solid colours and seven gradients. value is what to pass as a colour |
| openemail labels get <id> | Read one user label, with its id matched case sensitively |
| openemail labels create --name <value> | Create a user label. --color-background-color gives it a colour |
| openemail labels update <id> | Rename or recolour a label. The id stays, and so do the threads that carry it |
| openemail labels delete <id> | Delete a label and take it off every thread that carried it. Asks you to confirm |
- An id never changes, even after a rename, so store ids rather than names.
- System labels such as
INBOX,STARREDandUNREADare not listed and cannot be changed or deleted, thoughthreads updatetakes them.labels geton one is a 404. - A workspace holds up to 50 user labels. A name another label already has, compared without case, is refused with
label_name_taken. - A colour is a hex value such as
#3B82F6or a gradient token such asgradient:sunset.--label-colortakes the whole colour as JSON, and--label-color nullclears it. - A label belongs to the workspace, so renaming, recolouring or deleting it changes it for everyone in it.
labels deletehas no undo. Creating a label with the same name again gives the same id, but the threads do not get it back. ItsthreadCountinlabels getsays how many conversations will lose it.
How the mail commands use them
| Mail command | What it runs |
|---|---|
| inbox [folder] | threads list for one page, then threads get on each thread, six at a time |
| search <query...> | threads list --query, then threads get on each thread |
| read <thread-id> | threads get, then threads update --read unless you pass --no-mark-read |
| reply <thread-id> | threads get for the recipients, subject and sending address, then emails send into the thread |
| archive <thread-id...> | threads update --add-label-ids ARCHIVE --remove-label-ids INBOX |
| unarchive <thread-id...> | threads update --add-label-ids INBOX --remove-label-ids ARCHIVE |
| star, unstar <thread-id...> | threads update adding or removing STARRED |
| mark read, unread <thread-id...> | threads update --read, or --no-read |
| trash <thread-id...> | threads trash |
| snooze <thread-id...> --until <when> | threads snooze, with a delay such as 3h turned into an instant first |
| unsnooze <thread-id...> | threads unsnooze |
| label add, remove <thread-id...> | threads update --add-label-ids, or --remove-label-ids |
| send --draft <id> | emails send --draft-id |
- A mail command takes several thread ids and reports on each one, and with
--jsonit prints{ results, succeeded, failed }. A command on this page takes one id and prints what the API returns. openemail inboxreads every thread it lists to show who wrote last and the subject.threads listmakes one request per page and prints only ids, which is all a pipeline needs.openemail readturns an HTML message into text and marks the thread read.threads getprints the thread as the API returns it and changes nothing.
Examples
Mark a thread read, archive it and label it in one request, where mark read, archive and label add would make three:
openemail threads update CAHk7pQ2x9LmZ4 --read --add-label-ids ARCHIVE,USER_RECEIPTS --remove-label-ids INBOX --jsonMake a label and file every matching thread under it. Piped, --all prints one JSON object per line:
openemail labels create --name Receipts --color-background-color gradient:meadowopenemail threads list --query "in:anywhere subject:receipt newer_than:1y" --all | jq -r .id | xargs openemail label add --label USER_RECEIPTSSave one file from a message. The message ids are on the messages of threads get:
openemail threads get CAHk7pQ2x9LmZ4 --json | jq -r ".messages[].id"openemail threads list-attachments CAHk7pQ2x9LmZ4 message_4c1b257a --json | jq -r '.[] | select(.filename == "invoice.pdf") | .content' | base64 --decode > invoice.pdfWrite a draft, change it, read it back, then send it:
DRAFT=$(openemail drafts create --to [email protected] --subject "Engine notes for Thursday" --html "<p>Agenda below.</p>" --json | jq -r .id)openemail drafts update "$DRAFT" --to [email protected],[email protected]openemail drafts get "$DRAFT"openemail send --draft "$DRAFT" --from [email protected] --to [email protected],[email protected]Clear out drafts nobody has saved for 30 days. The dry run prints each DELETE without sending it, and --yes answers the confirmation:
openemail drafts list --query older_than:30d --all | jq -r .id > stale.txtxargs -n 1 openemail drafts delete --dry-run < stale.txtxargs -n 1 openemail drafts delete --yes < stale.txtPick a gradient from the palette, preview the change, make it, and later take the colour off again:
openemail labels list-colors --json | jq -r '.[] | select(.kind == "gradient") | .value'openemail labels update USER_RECEIPTS --name "Receipts 2026" --color-background-color gradient:aurora --dry-runopenemail labels update USER_RECEIPTS --name "Receipts 2026" --color-background-color gradient:auroraopenemail labels update USER_RECEIPTS --label-color nullScopes and verification codes
| Scope | Commands |
|---|---|
| threads:read | threads list, get and list-attachments |
| threads:write | threads update, trash, snooze and unsnooze |
| drafts:read | drafts list and get |
| drafts:write | drafts create, update and delete |
| labels:read | labels list, list-colors and get |
| labels:write | labels create, update and delete |
A missing scope stops with exit code 4. None of these commands asks for a verification code, with a browser sign-in or with an API key.
A sign-in or key limited to some addresses sees only the threads delivered to them, and any other thread is a 404, as if it did not exist. Labels belong to the workspace, so it still sees every label, but threadCount counts only the conversations it can see.
Pages, confirmations and dry runs
threads list,drafts listandlabels listread one page, 25 unless--limitsays otherwise, up to 100.--cursorcarries on from the cursor a page printed. A thread cursor keeps the order it was handed out in, so send the same filters with it.--allreads every page and--max <n>stops after that many. Piped or with--ndjsonit prints one JSON object per line, and with--jsonone{ items, hasMore, nextCursor }document.hasMorecan be true on what turns out to be the last page, and the next call then returns no items. A thread that gets new mail while you page moves ahead of the cursor and is not returned by later pages, and so does a draft saved while you page.threads trash,drafts deleteandlabels deleteask you to confirm. Unattended, with--json,--no-inputor no terminal, they stop with exit code2and change nothing unless you pass--yes.--dry-runprints the request a command would send, with the credential redacted, and exits with code0without sending it or asking to confirm. With--jsonit prints{ dryRun, request }.
JSON bodies and clearing a field
--data takes the whole body as JSON, inline, from a file with @path, or from stdin with -, and a flag you pass as well overrides its key.
An empty flag value is a usage error, so a field that an empty value clears goes through --data instead. --label-color null clears a label’s colour.
openemail drafts update draft-5f0c2a9e-8b1d-4e7a-a3c6-2d9f41b7e0c8 --data '{"from":""}'openemail drafts update draft-5f0c2a9e-8b1d-4e7a-a3c6-2d9f41b7e0c8 --data '{"threadId":""}'openemail drafts update draft-5f0c2a9e-8b1d-4e7a-a3c6-2d9f41b7e0c8 --data '{"to":[]}'openemail drafts create --data @draft.json --subject "Overrides the file"The first saves the draft with no sender, the second detaches it from the thread it replied to, and the third clears its recipients.
Every flag
openemail threads --helpopenemail threads list --helpopenemail drafts create --help --jsonopenemail <namespace> <verb> --help shows each argument and flag with its type, the scopes the call needs, its method and path, what it returns and the notes from the API reference. Add --json for the same help as data.