Document generation
How Oxagen agents author long-form documents through a vendor-neutral DocumentSpec — composing Google Docs from typed blocks, replacing placeholders, exporting PDF, and sharing with named users.
The document surface is the agent's long-form output channel. An
agent composes a typed DocumentSpec — title, blocks, sharing
intent — and the active provider materialises it as a Google Doc.
The provider contract lives in packages/oxagen/oxagen/documents/;
the agent never imports a vendor SDK and never sees raw OAuth
credentials.
The DocumentSpec shape
A DocumentSpec is a Pydantic model in
oxagen.documents.specs. Every spec is a sequence of typed
DocumentBlock entries; the provider walks the block list and
maps each kind onto the vendor's native element type.
| Block kind | Maps to (Google Docs) | Used for |
|---|---|---|
heading | Heading 1 / 2 / 3 paragraph style | Section titles |
paragraph | Plain paragraph with optional inline runs | Body text |
bulleted_list / numbered_list | List paragraph with bullet preset | Lists |
table | Native table with header row | Tabular data |
image | Inline image from a gs:// / https:// source | Diagrams, screenshots |
divider | Horizontal-rule paragraph | Section breaks |
placeholder | {{name}} token, later filled by docs.replace_placeholders | Templated briefs |
page_break | Page break | Multi-page reports |
Inline runs inside a paragraph carry bold / italic / underline / code / link annotations — no markdown round-tripping in the provider; the spec is the source of truth.
Capability surface
Every tool below is exposed on app, mcp, and api. The
connection_kind="google_workspace" binding rejects calls in
workspaces that have not connected Google with a structured
409 missing_connection response.
| Capability | Credits | What it does |
|---|---|---|
docs.create_from_spec | 5 | Materialise a DocumentSpec as a new Google Doc. Returns (external_id, external_url, kind='document'). Writes the corresponding row to app.documents. |
docs.append | 2 | Append a validated list of DocumentBlock entries to an existing document. |
docs.replace_placeholders | 2 | Substitute {{name}} tokens with caller-supplied values. Used by the agent to expand a templated brief in one round-trip. |
docs.export_pdf | 3 | Render the document to PDF and return the bytes. The PDF is also stored in app.documents with a parent_document_id pointing back at the source. |
docs.share | 1 | Grant a list of email addresses access at the chosen role (reader / commenter / writer). Idempotent — repeating with the same role is a no-op. |
Calling it
From an MCP client
{
"tool": "docs.create_from_spec",
"args": {
"spec": {
"title": "Acme account brief — Q2",
"blocks": [
{ "kind": "heading", "level": 1, "text": "Acme account brief" },
{ "kind": "paragraph", "text": "Acme has been on the platform since 2024-09." },
{ "kind": "heading", "level": 2, "text": "Open issues" },
{ "kind": "bulleted_list", "items": [
"ACME-235 — billing migration blocker",
"ACME-451 — SSO rollout"
]}
],
"share": { "emails": ["alice@acme.com"], "role": "commenter" }
}
}
}The agent gets back the doc URL plus the canonical document id in
app.documents. Subsequent docs.append, docs.replace_placeholders,
or docs.export_pdf calls reference the document by id.
From the REST surface
curl -X POST https://api.oxagen.ai/v1/cap/docs.create_from_spec \
-H "Authorization: Bearer $OXAGEN_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d @brief.jsonIdempotency-Key is the safe-retry contract: the second call
returns the cached (external_id, external_url) instead of
minting a duplicate doc.
Branding
When the workspace has a default BrandKit, the provider maps
its palette / typography / spacing tokens onto Google Docs'
named styles (Heading 1 / 2 / 3, Normal text, table header fill,
divider colour) before the blocks are inserted. The kit's logo
and footer markdown land in the doc header / footer. Generation
without an active kit falls back to Google Docs defaults.
See Brand kits for the kit shape and how to manage them.
Storage and provenance
Every successful docs.create_from_spec writes a row to
app.documents:
| Column | Value |
|---|---|
kind | uploaded |
extension | .gdoc |
mime_type | application/vnd.google-apps.document |
source | agent_generated |
provider | google |
external_id | Google Drive file id |
external_url | Drive viewer URL |
generated_by_run_id | The agent run that produced the doc |
The PDF emitted by docs.export_pdf lands as a sibling row with
parent_document_id pointing at the source doc — same
breadcrumb pattern used for connector-import revisions. See
Artifact storage.
Permissions and credentials
The provider reads credentials from the workspace's Google Workspace connection. The agent never sees raw tokens; the provider acquires a short-lived access token at call time and discards it after the round-trip. Token rotation, scope, and revocation behaviour match the rest of the Google connectors.
A workspace without a connected Google account that calls a
docs.* capability receives:
{
"error": "missing_connection",
"connection_kind": "google_workspace",
"candidates": []
}Audit
Every document generation writes an audit.event row keyed on
the (tenant_id, workspace_id) chain with action='document.created',
the DocumentSpec (truncated at 16 KB) in before, the
external reference in after, and the per-row hash chained to
the previous entry. The daily Merkle attestation job rolls the
chain up to a single root and pins it to GCS object-lock. See
Events, triggers, and audits.
Artifacts
How Oxagen agents author documents, spreadsheets, slides, and PDFs — and where every generated artifact gets stored, branded, and audited.
Spreadsheets
How Oxagen agents author Google Sheets through a vendor-neutral SheetSpec — typed headers, formatted cell ranges, charts from a typed ChartSpec, and PDF export.