Workspace documents
A workspace document is a persistent, structured table that accumulates state across every agent run. It is readable and directly editable by a human, and the agent can read and write to it between sessions. Both the human and the agent work on the same surface — the document is the shared record of where everything stands.
How this pattern emerged
When we built the first live campaign in Envelope — an MSP outreach campaign targeting Melbourne IT providers — we used a markdown file alongside the agent. That file did something the database couldn't: it was simultaneously the work queue, the status tracker, the briefing document, and the collaboration surface.
Rows moved through states. The agent found emails it had on record via Apollo. The human found a founder's email via pattern match on LinkedIn. Neither stepped on the other's work. The agent updated what it could and flagged what it couldn't. The human filled in what only they could find. When the next session started, the agent read the file, understood where things stood, and picked up exactly where things were left off.
That felt like collaboration, not automation. The file was the memory — both of where things stand and of everything learned along the way.
Envelope's state normally lives in the database. That's the right call for reliability, but it creates a gap: the state is opaque to humans. You can't open it, read it at a glance, make a small correction, and have the next run pick it up. The workspace document closes that gap.
The two layers
A workspace document has two layers, both present from the first session.
Structured state — a table of rows, each moving through declared stages. The agent reads it, does what it can, writes results back, and leaves unresolved rows clearly marked. The human fills in what only they can find. The document is the work queue and the status record in one.
Accumulated knowledge — freeform text below the table: source notes, decisions made, context the agent should factor in next time, findings that didn't fit in a column. This layer grows with every run. The agent reads it at the start of each session alongside the structured rows.
Nothing is lost between sessions. The document is the memory — both of where things stand and of everything learned along the way.
When to reach for it
The pattern fits any work with these properties: a list of items that move through states, a mix of human and agent contribution, and a goal that unfolds over multiple sessions.
Some examples of what this looks like in practice:
- Contact enrichment and outreach — rows move from
unverified → verified → sent → replied, the agent enriches email addresses and sends, a condition trigger fires when new unverified rows appear - Recruiting pipeline — candidates move from
applied → screened → interviewed → offered → closed, the agent writes interview notes and research after each stage, a trigger fires when a candidate goes stale - Sales tracking — deals move from
prospect → qualified → proposal → closed, the agent drafts outreach and does research at each stage, a trigger fires when a deal goes cold - Content calendar — pieces move from
idea → drafted → reviewed → published, the agent drafts on cue, a human approves before scheduling, a trigger fires when something is overdue - Vendor contract management — contracts as rows with renewal dates, a trigger fires 60 days before expiry, the agent prepares a renewal brief
The through-line: multiple entities, discrete states, a mix of agent work and human decisions, and the need to see the whole pipeline at a glance.
When not to use it: a genuinely one-shot task — a single lookup, a one-time report that runs once and is done — doesn't benefit meaningfully from a document. The conversation history is sufficient for that. The document's value materialises across sessions: when work spans multiple runs over days or weeks, when state needs to be readable at a glance without reconstructing it from history, and when both human and agent are contributing over time.
A practical guide: if a human would scroll through and read this table row by row, it's the right tool. If they'd only ever look at aggregate statistics, use a pipeline with structured outputs instead.
Scale ceiling: the document pattern works well at the scale of human-curated lists — tens to low hundreds of rows. Above a few hundred rows it starts to strain, for three reasons: the table consumes a meaningful portion of the model's context window, the "human readable" quality stops being used, and read/write latency grows. The signal that you've outgrown it: the human is no longer meaningfully reviewing individual rows, and the agent is processing them faster than any person could follow.
Declaring a document in your team definition
Add a workspace block to your team definition alongside schedule and pipeline:
{
"workspace": [
{
"name": "outreach-contacts",
"columns": [
{ "name": "company", "owner": "human", "id": true },
{ "name": "contact", "owner": "human", "pii": true },
{ "name": "email", "owner": "agent", "pii": true },
{ "name": "email-status", "owner": "agent" },
{ "name": "verified-via", "owner": "agent" },
{ "name": "send-status", "owner": "agent" },
{ "name": "sent-at", "owner": "agent" }
],
"statusValues": ["unverified", "verified", "needs-lookup", "sent", "replied", "bounced"],
"triggers": [
{ "column": "email-status", "status": "unverified", "action": "run" }
]
}
]
}The document is created automatically at install time, pre-populated with the declared schema. The human never sees a blank page.
Column ownership
Every column is declared as owner: "human" or owner: "agent". Agents can only write to columns they own — the runtime rejects writes to human-owned columns and logs the violation. This makes the data flow explicit in the team definition: you can read it and understand exactly what each agent contributes.
Set id: true on the column that uniquely identifies each row — the agent uses this to address a specific row when writing back. If no unique identifier is declared, the agent falls back to row index, which is fragile if the human edits the document between runs.
Mark columns containing personal data with pii: true. This flags them for appropriate handling in exports and API responses.
Status values
Status values are declared in the schema and enforced at runtime. The agent cannot write an undeclared value to a status column — invalid writes are rejected and surfaced in the room thread. This matters for condition triggers: when a trigger checks for rows with email-status: unverified, it can rely on that value being consistent across all rows.
Multi-stage status
For workflows with a clear action boundary, declare separate status columns for each stage rather than combining them in one. For example, email-status tracks pre-send verification state (unverified → verified → needs-lookup), and send-status tracks post-send outcome (pending → sent → delivered → bounced → replied). Condition triggers can then target each stage independently.
Source attribution
For workflows where an agent populates data a human will use or publish, a binary verified/unverified status is often not enough. Declare a verified-via column to record where data came from: apollo, pattern-match, manual, or unsourced. This makes agent writes auditable and helps the human know how much to trust each row.
Built-in row fields
Two fields are always available on every row, regardless of what you declare in the schema:
| Field | Owner | Description |
|---|---|---|
| notes | human | Free-text context the agent reads and factors in but never overwrites. Use this to tell the agent something specific about a row: "do not follow up," "mention the case study if they reply," "email bounced — do not retry." |
| blocked-by | human | A dependency hold. A string describing what must happen before this row can proceed: "hold until first client signed," "wait for legal review." Rows with a non-empty blocked-by are skipped entirely by the agent and excluded from trigger evaluation. Clear by setting to "" or null. |
The agent reads notes at the start of each run and factors them into its decisions for that row. It never overwrites a notes field. This is worth first-classing in the document rather than leaving as an informal convention in the prompt — otherwise it gets reinvented differently in every team.
The document panel
The panel is hidden by default and opened from the room header. The document accumulates regardless of whether the panel is open. State is tracked, agent writes land, decisions are recorded — all before the user has ever opened the panel. When they do open it, the document reflects everything that has happened since the team was installed, not just from the moment the panel was first viewed.
The panel shows the current state of every row: what the agent has filled in, what is still pending, what is blocked, and what has reached a terminal state. Direct cell editing is supported — click a cell, edit it, save — without going elsewhere to fill in missing data or clear a blocked-by field.
When an agent completes a batch run, it outputs a JSON summary of changed rows. SmartOutput renders this inline in the thread as a diff table — you can see exactly what moved and to what state without opening the panel. The panel is there for deeper inspection, not for reading every run result.
The session-open brief
When you open a room with an active workspace document, the agent opens with a structured status brief rather than a blank prompt. This is the highest-value moment in the whole pattern — it's what makes the product feel like a colleague rather than a chatbot.
The brief follows this structure, always in this order:
- Status counts — one line. All non-terminal states with counts. "4 verified, 3 need lookup, 1 sent."
- Blocked items — the actual rows the agent cannot proceed without human input, each with a concrete reason. Not a count — the specific items and why they're stuck.
- Ready to proceed — what the agent can do next in an autonomous run without any human input. One sentence. If nothing is actionable, it says so explicitly.
- One clear prompt — a single question or action offer.
Example:
You have 4 contacts verified and ready to send, 3 still need a direct email found, and 1 is on hold.
Need your help with: Todd Afford at Intech Group — no email found via Apollo or pattern match. Sarah Chen at Blueshift — Apollo returned two addresses, unclear which is current.
I can do next: Send the 4 verified contacts autonomously whenever you're ready.
Ready to send the verified 4, or do you want to review them first?
The brief is shorter when there is nothing blocked and nothing the agent can do — status counts and a prompt to add new rows. If the document is Complete, the brief surfaces that and offers to archive.
Write-back
When an agent completes an action on a row — sends a message, publishes an article, files a report — it writes back to the document immediately. A human who triggers an action and then has to manually update the document is experiencing the pattern as overhead rather than automation. Write-back is a hard requirement, not optional. The document is the product the deployer sees.
Write-back has two distinct modes:
During execution (scheduled or triggered run) — automatic and silent. The agent verifies an email, it writes email-status: verified immediately. The send agent dispatches a message, it writes sent-at immediately. No prompt, no confirmation.
During conversation — not automatic. Conversation is exploratory. The agent does not silently write conversational changes to the document. A signal is required: the human says "update the doc with that," the agent asks at a natural pause "should I add that to the document?", or the change is clearly implied. The agent is always explicit about when it is writing to the document from a conversation.
Routing conversational changes
With a document in play, conversational changes have two possible destinations — the document or the team definition — and the agent routes them correctly.
State and knowledge → document. Adding rows, updating notes, recording a decision, capturing a finding, marking something done. These are operational changes — they reflect where the work stands and what has been learned.
Behaviour and structure → team definition. Changing how the agent works — adding a tool, restructuring the pipeline, changing a trigger, altering a core instruction. These are architectural changes.
The team definition stabilises after the initial build. Most day-to-day conversational changes are operational and land in the document. The agent names what it changed either way: "I've added those contacts to the document" or "I've updated the team instructions to include that step."
When a change sits on the boundary — "let's also track LinkedIn profiles for each contact" — the agent surfaces the ambiguity rather than guessing: "I can add a LinkedIn column to the document and update the enrichment agent to look up LinkedIn profiles as part of its run — want both, or just one?"
Document lifecycle
| State | Description | |---|---| | New | Document exists but has no rows yet. The agent acknowledges it at session start but doesn't treat it as actionable. Condition triggers do not fire. | | Active | The document has rows in progress. Condition triggers may be watching it. The agent reads it at session start and includes it in context. | | Complete | All rows have reached a terminal state. The agent surfaces this and offers to archive or keep as a record. The document does not close itself silently. | | Archived | Retained as a read-only record. No longer injected into session context by default, but accessible if the human asks. |
On team uninstall, documents are archived by default — the data outlives the team definition. Export to CSV or JSON is available in any lifecycle state from the room thread.
Version history and rollback
Every agent write batch creates a named snapshot. If the agent makes a bad write — clears a column, overwrites correct data, corrupts a row — revert to any prior snapshot from the room thread in one step. This is a trust feature: a human will only let an agent write to a shared document if they know they can undo it.
Inbound state transitions
Row state can also be updated by inbound events from outside Envelope:
Reply tracking — when a contact replies to an outbound email, Envelope's reply-to routing matches the sender's email address to a document row and transitions send-status from sent → replied automatically. The outreach sequence stops. No manual update required.
Webhook transitions — any external system with a webhook can trigger a row state change. A CRM marks a contact as "meeting booked" — that webhook hits Envelope's inbound route, matches the contact to a row by the declared unique identifier, and updates the status. The document becomes the integration target, not just the data source.
The inbound webhook infrastructure is part of Envelope's v1.1.0 release. Document row transitions wire into it — the handler receives an inbound event, extracts the identifier field, and applies the declared transition rule.
Document-backed condition triggers
Teams that use workspace documents can declare condition triggers directly in the document schema, targeting row state rather than polling an external tool:
"triggers": [
{ "column": "email-status", "status": "unverified", "action": "run" }
]This is more reliable and cheaper than a freeform condition prompt — the trigger evaluates a lightweight status index rather than calling a model. The index is updated atomically on every row write; condition polling reads the index, not the full document body.
Rows with a non-empty blocked-by field are excluded from trigger evaluation regardless of their status value. The trigger does not fire on empty documents.
Multiple documents per install
An install can declare multiple workspace documents. For example, an outreach team might declare a separate contact list and a send log — the contact list tracks enrichment state, the send log records every dispatched message as its own row. Each document has a unique name within the install, used as the document parameter in document_read and document_write tool calls.
PII and data compliance
Documents frequently contain sensitive data — contact names, email addresses, client lists. Mark personal data fields with pii: true in the column declaration. This flags them for appropriate handling in exports and API responses.
Row-level deletion is supported for right-to-erasure compliance — individual rows can be removed without affecting the rest of the document. Soft deletion (marking a row as deleted) is not sufficient for compliance; the data is physically removed. Archived documents support row-level deletion for the same reason.