Sheet Workflows
A workflow reads a range from one Google Sheet, applies a transform, and writes the result — on a schedule or on demand. Try it in the web app, or drive it through the REST API.
Describe it in plain English
Instead of picking a mode and filling in fields by hand, describe what you want once — "count orders per rep and write the totals to Summary" — and POST /api/workflows/compileturns it into a structured, editable spec (mode + fields), grounded against your actual column names so it won't invent ones that don't exist. This is a one-time AI parse (1 credit) — the resulting workflow itself runs on schedule with whatever cost its mode implies, not the compile cost, every time.
curl https://thepaperroom.co/api/workflows/compile \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"description": "count orders per rep", "columns": ["Rep", "Order ID", "Amount"]}'Modes: only ai_transform costs credits
Every mode below is picked with the mode field on create. Six of the seven modes are exact, deterministic Go code (or a plain HTTP fetch, for api_call) with no model call — the credit gate (RunGated) only ever wraps the AI path, so scheduling a daily aggregate, formula, or api_call job costs nothing no matter how often it runs.
aggregateFreeGroup rows by 1–2 columns and count/sum/average/min/max another column — a pure Go computation, no AI involved.
Fields: groupByColumn, groupByColumn2 (optional), aggregateColumn, aggregateOp (count | sum | avg | min | max)
dedupeFreeFlag duplicate rows by one or more key columns (case-insensitive match), keeping every row and marking later duplicates.
Fields: dedupeKeyColumns (comma-separated column names)
lookupFreeJoin the source sheet against a second lookup sheet by matching key columns, pulling in specified columns — like VLOOKUP as a scheduled job.
Fields: lookupSpreadsheetId, lookupRange, lookupKeyColumn, lookupSourceKeyColumn, lookupReturnColumns
cleanupFreeApply deterministic text ops (trim, uppercase, lowercase, title-case, phone-format) to selected columns.
Fields: cleanupColumns, cleanupOps — both comma-separated and applied to every selected column uniformly, so don't mix e.g. phone with a Name column
formulaFreeCompute a new column from a spreadsheet-style arithmetic expression referencing other columns as {Column Name} — +, −, ×, ÷, ^, parentheses. A bad reference or division by zero shows #ERROR on that row rather than failing the whole run.
Fields: formulaExpression (e.g. "{Price} * {Quantity} * 1.1"), formulaOutputColumn
api_callFree (no AI — it's an HTTP fetch)GET a URL per row (placeholders substituted from {Column Name}, capped at 200 rows/run) and pull one field out of the JSON response into a new column. Requests to loopback/private/link-local addresses are blocked server-side.
Fields: apiCallUrl (e.g. "https://api.example.com/lookup?id={Customer ID}"), apiCallResponsePath (dot-path, optional), apiCallOutputColumn
gmail_import1 credit per new email (capped at 20/run)Polls your connected Gmail inbox (read-only) for messages matching a search query, extracts CRM-ready fields from each with AI, and appends new rows to the destination sheet — never overwrites, and never re-imports a message it's already processed.
Fields: gmailImportQuery (Gmail search syntax, e.g. "is:unread label:leads") — sourceSpreadsheetId/sourceRange are unused for this mode
outlook_import1 credit per new email (capped at 20/run)The Microsoft Graph mirror of gmail_import — polls a connected Outlook/Microsoft 365 account (read-only) for messages matching a search query, extracts CRM-ready fields with AI, and appends new rows. Requires connecting Outlook separately from Google Sheets (see the account/workflows page).
Fields: outlookImportQuery (Microsoft Graph $search syntax, e.g. "isRead:false") — sourceSpreadsheetId/sourceRange are unused for this mode
email_campaignFree (no AI — a template fill + an HTTP send)Sends a personalized email to every new recipient in the source range via your connected Gmail or Outlook account — never the same address twice, even across runs. Capped at 30 recipients/run, paced 1/second. Pair with a "once" schedule for a single scheduled send, or a recurring schedule for an evergreen campaign that only emails newly-added rows.
Fields: campaignSubject, campaignHtml (both support {{Column Name}}), campaignProvider ("gmail"|"outlook"), campaignRecipientColumn — doesn't support requireApproval
ai_transform3 credits per runThe one mode that calls the model — for transforms that need judgment (summarizing, categorizing, extracting) rather than exact logic. This is the only mode gated through the credit system; the four above never touch it.
Fields: description (plain-English instruction for the AI)
Approval gate
Set requireApproval: true and a run computes its output, holds it as pending_approval with a before/after diff, and waits — nothing is written to the destination sheet until you call POST /api/workflows/runs/{runId}/approve. Pending approvals auto-expire after 48 hours and default to rejected, never to a silent auto-write.
Notifications
Set notifyEmail (via your connected Google account), notifyOutlookEmail (via your connected Outlook account), and/or notifySlackWebhookUrl to get a message when a run finishes, errors, or needs approval — set more than one to notify multiple places at once. Both email paths are send-only (neither ever reads your mail); Slack is a plain incoming webhook POST, no OAuth app install needed. All are best-effort: a failed notification is logged but never fails the underlying workflow run.
Audit log
Every run — scheduled, manual, or an approval decision — is recorded with row counts and a before/after snapshot. Pull it via GET /api/workflows/audit-log, or add ?format=csv for a download, from the API or the web page.