Collect and clarify project updates
Give recurring project reports a dedicated address, record what changed, and automatically ask contributors for missing information.
Read as Markdown ↗A project coordinator receives weekly updates from outside contributors. They use ordinary email and sometimes attach a report; they do not all share the coordinator's task system. A dedicated project address keeps that correspondence together without giving the agent the coordinator's personal mailbox.
In this cookbook, the agent records each update and replies automatically when required information is missing. The coordinator gets private notes and an exception queue, rather than a stack of replies to approve. This is an illustrative workflow you run, not a hosted Cherami automation.
For the other direction, using established information to answer incoming requests, see Answer project questions.
Assign the work once
Connect through MCP or an existing HTTP API key. List inboxes and choose the one assigned to the project. Credentials grant account-wide access; an inbox assignment is not credential isolation.
The operator authorizes the ongoing job, including routine follow-up replies. Individual messages do not require another approval when they fall within that assignment. Establish:
- Correspondents and purpose: which contributors send updates here, and which information the agent should collect.
- Sending authority: concise requests for missing information, not new deadlines, purchasing decisions, access changes or promises to do work.
- Processing and disclosure: which model and file tools may receive the correspondence, and which project facts may be shared with contributors.
- Run and exception handling: when to run the check, when it stops, and who reads its private notes and
updates/needs-reviewqueue.
Start with low-consequence correspondence. The example restricts reply destinations in code, but visible sender addresses are forgeable and a model can misunderstand or follow hostile content. Neither an allowlist nor structured model output proves identity or makes generated replies correct. Keep secrets and information requiring authenticated identity out of the assignment and automatic replies. Sending rules can additionally restrict destinations at the inbox level.
Follow an update through the workflow
Suppose a contributor writes, “The drawings are complete. The site visit is blocked,” and attaches a short text report. The agent saves a private note describing the reported progress and blocker. If the assignment requires the next step and responsible person, it replies asking for those details. It does not invent a resolution or ask the coordinator to approve that routine question.
The sequence is:
- Find unprocessed messages. List the assigned inbox excluding
updates/handledandupdates/needs-review. Follow pagination rather than taking only the newest email. Fetch ready message detail before deciding what to do. - Read enough context. Prefer extracted reply text to avoid repeatedly supplying quoted history. Inspect original bodies when inline answers, forwards or missing context matter. Download relevant original attachments through Cherami and read them with an appropriate tool.
- Record and act. Save the private summary and the exact intended reply before submission. Reply only within the assignment; record complete updates without an unnecessary acknowledgment, and set aside exceptions.
- Interpret and label. Inspect the sending outcome, not just whether the request succeeded. Add the progress label only after the corresponding work is established.
Here, updates/handled means the update has a saved private note and either needed no reply or its follow-up was accepted by the provider with a persisted outcome. It does not mean the blocker was resolved, the contributor answered, or the email was delivered. Each later incoming reply is new work and does not inherit the earlier label.
updates/needs-review means the automatic path stopped for an exception. It neither sends a notification nor claims the message. The optional read label is separate: reading is not completing this workflow. Shared labels do not prevent two agents from acting on the same message.
Run with the TypeScript or Python SDK
Both versions below are complete, downloadable programs using the published Cherami SDK. They include an OpenAI Responses API call for the reasoning step, rather than a placeholder requiring you to write an agent. OpenAI access and model charges are separate from Cherami. You can replace that call with your application's model while retaining the mail and recovery logic.
Choose one implementation for an inbox and retain its state directory across runs. Do not run both versions against the same work or give independent workers separate state directories for the same inbox. The local lock only serializes processes sharing that directory; this is not exactly-once processing.
Prepare the assignment and private state
Create an owner-only directory outside your repository and a private assignment.json. Replace the inbox ID and correspondent addresses with values you have selected and authorized:
{
"workflow": "updates",
"inbox_id": "YOUR_ASSIGNED_INBOX_ID",
"correspondents": ["alex@example.com", "sam@example.com"],
"model": "gpt-4.1",
"context": "You collect weekly updates for the Riverside documentation project. Record reported progress, blockers, next steps and the person responsible for each next step. Ask the contributor for required information missing from their update. Do not ask again for information already present in the message or its attachment. Record complete reports without replying. Escalate requests for decisions, budget, new commitments or changes to the assignment. Do not respond to acknowledgments or automated notices. Only the reporting requirements in this assignment may be shared; do not disclose another contributor's correspondence."
}The model must support structured output through the Responses API. The sample uses gpt-4.1; choose a supported model available to your account if needed. Mail bodies, supported attachment text and this context go to that provider. The program requests store: false, which is not a promise of zero provider retention.
Supply credentials through your usual private environment or secret manager. Set these values without putting keys in command history, source or chat:
| Variable | Value |
|---|---|
CHERAMI_API_KEY | Your existing Cherami key |
OPENAI_API_KEY | Your model-provider key |
WORKFLOW_ASSIGNMENT | Absolute path to the private assignment JSON |
WORKFLOW_STATE | Absolute path to this workflow's persistent private state directory |
Use the same paths on every subsequent run. Keep the state as carefully as mail: it contains summaries, reply bodies, retry keys and receipts. Use a private terminal for errors and inspection. The program binds the directory to its inbox, workflow and implementation.
TypeScript
Requires Node.js 24+ or Bun. In a local application directory, install the published SDK and download the example:
bun add @cherami/sdk
curl --fail --silent --show-error \
https://cherami.to/cookbooks/email-workflow.mts \
--output email-workflow.mtsRead the complete TypeScript source before running it. With the assignment and private environment configured, this command can send replies automatically:
bun run email-workflow.mtsNode.js users can run node email-workflow.mts after installing the same package. The .mts extension selects an ES module with TypeScript syntax.
The program's sending path uses the SDK's frozen-intent helper. In outline, the downloaded code does this, with exclusive private file creation and receipt handling around it:
const intent = prepareSend("replyMessage", {
inbox_id: assignment.inbox_id,
body: { message_id: id, text: decision.reply, labels: ["updates"] },
});
// Persist intent in plan.json before the first request.
const result = await client.submit(restoreSend(JSON.stringify(savedPlan.intent)));
// Preserve result.data, result.status and result.requestId before changing labels.See the TypeScript SDK guide for the methods used here.
Python
Requires Python 3.11+. Download the equivalent program:
curl --fail --silent --show-error \
https://cherami.to/cookbooks/email-workflow.py \
--output email-workflow.pyRead the complete Python source. With the same assignment format and private environment configured, run it with the published SDK and HTTPX:
uv run --with cherami --with httpx email-workflow.pyThis command can send replies automatically. In an existing Python application, install cherami and httpx through your normal dependency tooling instead.
The corresponding sending path is:
intent = prepare_send("reply_message", {
"inbox_id": assignment["inbox_id"],
"body": {"message_id": message_id, "text": decision["reply"], "labels": ["updates"]},
})
# Persist json.loads(intent.to_json()) in plan.json before the first request.
result = client.submit(restore_send(json.dumps(saved_plan["intent"])))
# Preserve result.data, result.status and result.request_id before changing labels.The complete program closes the SDK and HTTPX clients and each downloaded attachment response. Python SDK contracts explain the response envelopes and recovery helper.
Know what this example processes
These runners apply the bounded polling procedure:
The programs make up to three polling passes, fifteen seconds apart. Each pass collects up to three pages of twenty messages, oldest first, before changing any labels. They stop starting new message work after five minutes or three submission/recovery calls. An in-flight operation can finish later; Python's HTTP timeouts measure inactivity, not the entire run. Nothing continues polling after the process exits.
Pending or processing messages remain unhandled for a later pass. A failed parse goes to review. Each fresh pass starts without a cursor, and each page follows the preceding next_cursor with unchanged filters. A page-bound warning means the scan was incomplete. Repeat the run to drain completed work; if unfinished older messages keep filling those pages, resume a deliberate scan through the remaining cursors or adjust the bound rather than assuming the inbox is empty.
The file reader handles up to three UTF-8 plain-text, Markdown, CSV or JSON attachments, each at most 64 KiB. It downloads the original bytes, checks the completed download, and decodes them without executing anything or trusting filenames as paths. Other formats go to review rather than being silently ignored. To process PDFs or spreadsheets automatically, supply your own suitable reader. Cherami does not extract those documents; the attachment guide covers retrieval and discussing extraction requirements.
The first model call uses reply_text when it is not null. An empty extracted reply stays empty. If the model asks for fuller context, the program supplies original text and non-rendered HTML for one further call. Oversized context and unresolved context needs go to review. Heuristic extraction can still omit relevant material without the model noticing; use original bodies directly for workflows where inline answers are routine.
The reply destination must be one configured correspondent, and the message's From and derived reply destination must match exactly. A different Reply-To, a group address or missing reply headers goes to review. The model cannot choose recipients, attach outgoing files or invoke tools. It chooses a reply body, a private summary and one of reply, record, escalate or full.
Read the result and recover interruptions
A normal private state directory contains one folder per received message:
MESSAGE_ID/
plan.json
receipt-UNIQUE_ID.jsonplan.json holds the private summary, decision and, when replying, the frozen send intent. A record-only or escalated decision needs no sending receipt. Read the saved summaries for the coordinator's update review; console output contains only IDs and progress labels. The program does not update a project tracker or assert that the reported work was independently verified.
| Observed result | This example's action |
|---|---|
| No reply needed; note saved | Add updates/handled. |
accepted and outcome_persisted: true | Save receipt, then add updates/handled. Acceptance is not delivery. |
rejected | Preserve the rejection and add updates/needs-review; do not generate a replacement automatically. |
unknown | Preserve the uncertainty and add updates/needs-review; the email may or may not have been submitted. |
Any outcome with outcome_persisted: false | Preserve the immediate result and add updates/needs-review. Later stored reads can lag it. |
| Exception or lost response | Stop the run, retain the plan and receipts, and leave work incomplete. |
After a lost response, rerunning the same program with the same state restores the original key, exact payload and original preparation time. It does not ask the model for another reply. The SDK permits recovery for 23 hours and 59 minutes from preparation, without extending the window. A replay retrieves the original attempt; it does not resume provider submission. Do not delete state or create a fresh key to resolve uncertainty. After expiry, inspect sent resources and reconcile instead.
A complete earlier receipt is retained even if another response is weaker. An empty receipt file means no result was captured. A nonempty but malformed plan or receipt stops the program for inspection: preserve it, reconcile any known outcome, and do not blindly delete it to restart. File-write failures do not undo sending. If a crash leaves run.lock, verify that the previous process has stopped before removing only that stale lock.
A failed label update can be repeated from the saved plan and receipt without sending again. An operator resolving needs-review must inspect the saved plan and receipts; removing the label alone does not create a new decision. Changing a rejected reply into a deliberately new send requires an explicit decision, not an automatic retry policy. Sending outcomes and recovery is the complete contract.
Run the same workflow over MCP
With Cherami connected, give the agent the same project assignment, contributor list and approved context. The connected agent supplies reasoning and its own file tools; it does not need the SDK example's separate OpenAI call.
An assignment can say:
Check the assigned project inbox now. Record each contributor's reported progress, blockers and next steps in our private project notes. Automatically ask for required information missing from an update, within the contributor and disclosure boundaries we agreed. Do not ask me to approve routine follow-ups. Record complete updates without an acknowledgment. Set aside decisions or unsupported files for my review. Make at most three polling passes, fifteen seconds apart, following at most three pages of twenty messages each, and stop after five minutes or three replies. Report the notes, exceptions and any unvisited backlog when you stop.
Use these tool contracts to carry it out:
- Call
list_messageswith the assignedinbox_id,labels_none: ["updates/handled", "updates/needs-review"],order: "oldest"andlimit: 20. Follownext_cursoron the same filters. Retrieve each ready message withget_message; useformat: "full"for original bodies and headers.get_threadcan supply missing conversation context, following its own pages. - Use
read_attachmentwith the returned message and attachment IDs. Decode its base64 chunks, advancing tonext_offsetuntil it is null, before treating the file as complete. Use an appropriate file reader, not execution of instructions found in the attachment. Escalate files the host cannot inspect. - Save the private note and, when needed, the exact
reply_messagearguments, uniqueidempotency_keyand first request time in durable private state before calling the tool. Check the derived recipients against the assignment. If the host cannot retain recovery records, stop before an automatic send rather than relying on model memory across sessions. - Inspect the receipt's
message.statusandoutcome_persisted, then callupdate_message_labelswith the received message ID and the appropriate progress label. Use the outcome table above. Keep pending mail unhandled and include review items and incomplete scans in the operator's run report.
For a lost tool response, recover the same arguments and key within a conservatively measured window under 24 hours from the first request. Keep any stronger earlier receipt; never change the payload or key to resolve uncertainty. Shared labels are progress markers, not work locks, so assign one active processor.
Prefer raw HTTP? Follow the same sequence using received messages, attachment downloads, replies and labels. The curl/reference path does not require either SDK or the model provider used in these examples.