Gent API Documentation

Event Notifications

Subscribe to inbox events and receive notifications. Event notifications cover operational events — incoming messages, label changes, cap thresholds — and contact date events such as birthdays and anniversaries, which fire once per year on the matching date. Two delivery types are supported: webhook (HTTP POST to a URL) and task (creates a task in the inbox's Tasks calendar).

Testing webhook integrations against the demo server? See Demo server behavior for session isolation, built-in webhook sinks, sample event emission, TTL cleanup, and demo-only limits.
GET /v1/events/reference/ List available event types  no auth required; admins see admin-only types
200 OK
{ "version": "2026-06-06", "resource": "events", "payload": { "list_events": { "method": "GET", "path": "/v1/events/" }, "subscribe_alert": { "method": "POST", "path": "/v1/alerts/" } }, "fields": [...], "builder": { "group_field": "category", "value_field": "type" }, "events": [ { "type": "message.received", "category": "Messaging", "description": "A new inbound message arrives in the inbox.", "config": [], "admin": false }, { "type": "event.created", "category": "Calendar", "description": "Time-based reminder for upcoming calendar events.", "config": ["offset"], "offset_units": ["minutes", "hours", "days"], "admin": false } ], "categories": { "Messaging": ["message.received", "message.sent"], "Calendar": ["event.created", "event.updated"] } }
Unauthenticated and member callers receive the public catalog. Tenant owners/admins also receive tenant-admin billing, account, threshold, and approval event types marked with "admin": true.
GET /v1/events/types/ List event type options  no auth required; admins see admin-only types
{ "resource": "event_types", "events": [...], "categories": {...}, "counts": {"events": 24, "categories": 8} }
GET /v1/alerts/?inbox_id= List event notifications for an inbox  scope: alerts:read
[ { "id": "<notification-id>", "inbox_id": "<inbox-id>", "events": ["contact.birthday"], "delivery": { "type": "webhook", "target": "https://your.app/hook" }, "offset": { "value": 2, "unit": "days" }, "threshold": null, "filter": null, "enabled": true, "retries": 3, "backoff": "exponential", "created_at": "2026-05-15T10:00:00Z", "updated_at": "2026-05-15T10:00:00Z" } ]
POST /v1/alerts/ Create an event notification  scope: alerts:write
Request — time-based (with advance notice)
{ "inbox_id": "you@example.com", "events": ["contact.birthday"], "delivery": { "type": "webhook", "target": "https://your.app/hook" }, "offset": { "value": 2, "unit": "days" }, "filter": { "contact_id": "<contact-id>" }, "retries": 3, "backoff": "exponential" }
FieldTypeRequiredDescription
inbox_idstringrequired
eventsarrayrequired
offsetobjectoptionalFire before the event — e.g. {"value": 2, "unit": "days"}.
filterobjectoptional
retriesnumberoptionalDefault 3.
backoffstringoptionalOne of: immediate, fixed, exponential.
Request — task delivery (creates a task on alert)
{ "inbox_id": "you@example.com", "events": ["message.received"], "delivery": { "type": "task", "target": "New message: {type}" } }
FieldTypeRequiredDescription
delivery.typestringrequiredSet to task.
delivery.targetstringrequiredSummary template — supports {field} interpolation from the event payload.
Request — threshold-based
{ "inbox_id": "you@example.com", "events": ["cap.reached"], "threshold": 80, "delivery": { "type": "webhook", "target": "https://your.app/hook" } }
FieldTypeRequiredDescription
thresholdnumberoptionalPercentage of cap for cap.reached events.
201 Created
{ "id": "<notification-id>", "inbox_id": "<inbox-id>", "events": ["contact.birthday"], "delivery": { "type": "webhook", "target": "https://your.app/hook" }, "offset": { "value": 2, "unit": "days" }, "threshold": null, "filter": null, "enabled": true, "retries": 3, "backoff": "exponential", "created_at": "2026-05-15T10:00:00Z", "updated_at": "2026-05-15T10:00:00Z" }
GET /v1/alerts/{id}/ Get an event notification  scope: alerts:read
{ "id": "<notification-id>", "inbox_id": "<inbox-id>", "events": ["contact.birthday"], "delivery": { "type": "webhook", "target": "https://your.app/hook" }, "offset": { "value": 2, "unit": "days" }, "threshold": null, "filter": null, "enabled": true, "retries": 3, "backoff": "exponential", "created_at": "2026-05-15T10:00:00Z", "updated_at": "2026-05-15T10:00:00Z" }
PATCH /v1/alerts/{id}/ Update an event notification  scope: alerts:write
Request — all fields optional
{ "events": ["contact.birthday"], "delivery": { "type": "webhook", "target": "https://your.app/hook" }, "offset": { "value": 2, "unit": "days" }, "threshold": 80, "filter": { "contact_id": "<contact-id>" }, "enabled": true, "retries": 3, "backoff": "exponential" }
FieldTypeRequiredDescription
eventsarrayoptional
deliveryobjectoptional
offsetobjectoptional
thresholdnumberoptionalPercentage of cap for threshold-based events.
filterobjectoptional
enabledbooleanoptional
retriesnumberoptional
backoffstringoptional
Response
{ "id": "<notification-id>", "inbox_id": "<inbox-id>", "events": ["contact.birthday"], "delivery": { "type": "webhook", "target": "https://your.app/hook" }, "offset": { "value": 2, "unit": "days" }, "threshold": null, "filter": null, "enabled": true, "retries": 3, "backoff": "exponential", "created_at": "2026-05-15T10:00:00Z", "updated_at": "2026-05-15T10:00:00Z" }
DEL /v1/alerts/{id}/ Delete an event notification  scope: alerts:write
# 204 No Content — empty body
→ Event notification field reference
Delivery types: webhook — POST a JSON payload to the target URL; supports retries and backoff. When the deployment has GENT_WEBHOOK_SECRET configured, event notification webhooks include X-Gent-Signature: sha256=<hex>. Event notifications do not accept a per-alert secret field. task — create a task in the inbox's Tasks calendar; target is a summary template that supports {field} interpolation from the event payload (e.g. "Alert: {type}"); retries and backoff are ignored.
Event types
Event typeCategoryFires whenConfig
Messaging
message.receivedMessagingA new inbound message arrives in the inbox
message.sentMessagingAn outbound message is sent successfully
message.deliveredMessagingRecipient mail server confirmed delivery. Payload: message_id, thread_id, recipients[] (address, smtp_reply)
message.bouncedMessagingOne or more recipients rejected the message. Payload: message_id, thread_id, recipients[] (address, smtp_reply, bounce_category). Hard bounces auto-suppressed.
message.missed_replyMessagingA sent message has received no reply after the inbox's missed_reply_threshold_days. Payload: message_id, to_address, days_since_sent. Auto-subscribed when threshold is set.
message.spam_complaintMessagingA recipient marked a sent message as spam. Payload: recipient, feedback_type. Recipient is auto-suppressed. Requires spam feedback registration with each ISP.
Labels
label.appliedLabelsA label is applied to a message or contact
label.suggestedLabelsLabel inference pipeline finds suggestions for a new message. Payload: message_id, suggestions[] (label_id, label_name, confidence). Requires label_suggest feature.
Calendar
event.createdCalendarTime-based reminder scanned hourly — fires at scheduled start minus offset. Fires for all upcoming non-cancelled events. Units: minutes · hours · days.offset
event.updatedCalendarSame as event.created but distinct stream for modified events.offset
event.cancelledCalendarFires immediately when an event is deleted or patched to status: "cancelled". offset shifts relative to original start.offset
event.suggestedCalendarMeeting/call detected in inbound message. Payload: message_id, title, start. Requires calendar_suggest feature.
Contacts & relationships
contact.{type}AnniversaryScanned daily. Fires when a contact's anniversary month/day matches today (or today − offset days). E.g. contact.birthday, contact.work_anniversary. Use contact.any to match all types. offset only supports unit: "days". Deduped once per calendar year per contact. Optional filter: {"contact_id": "..."}.offset (days only)
contact.share_receivedContactsA contact share offer or request arrives. Fires on the recipient inbox (offer) or contact-holder inbox (request).
contact.handoff_receivedContactsA contact has been handed off to this inbox by a team member.
Thresholds & account
cap.reachedThresholdSend or spend cap crosses the configured levelthreshold (% of cap)
storage.thresholdThresholdMessage and attachment storage crosses the configured levelthreshold (% of limit)
auth.failedThresholdConsecutive API auth failures reach the countthreshold (count)
billing.cost_thresholdBillingMonthly spend crosses a USD amount. Edge-triggered — fires once per calendar month.threshold (USD)
billing.projected_overageBillingProjected month-end spend (linear extrapolation) crosses a USD amount.threshold (USD)
domain.expiringAccountA registered domain is nearing its expiry date
Security & intelligence
phishing.detectedSecurityInbound message classified as phishing (confidence ≥ 0.8). Payload: message_id, score.
email.high_priorityIntelligenceInbound message classified as high priority by the AI layer. Payload: message_id. Requires next_step feature.
Workflow
workflow_rule_matchedWorkflowA workflow's notify action fired. Payload: inbox_id, message_id, trigger, context.
approval.*Approvalsapproval.queued — action held for review. approval.approved — approved and executed. approval.rejected — rejected. Payload: approval_id, action_type, summary.
Files
file.createdFilesA file was created. Payload: file_id, name, file_type, folder_id, source_message_id, collaborators.
file.updatedFilesFile metadata was updated. Payload: file_id, name, file_type, collaborators.
file.version_addedFilesNew content version saved. Payload: file_id, name, version_number, source_message_id, collaborators. Fires on update_document workflow write and direct uploads.
file.rows_appendedFilesRows appended to a spreadsheet. Payload: file_id, name, rows_added, source_message_id, collaborators. Fires on append_rows workflow write and direct API writes.
file.deletedFilesA file was permanently deleted. Payload: file_id, name, file_type.
Workflows

Event-driven rules that perform writes when inbox or tenant events occur. Each rule has a trigger, optional conditions to filter the event context, and one or more source-to-target writes. Rules are owned by the creating actor — only the creator can read or modify them. Endpoints in this section accept session auth or an agent token with the required workflow scope.

GET /v1/workflows/reference/ List workflow builder options  scope: workflows:read
200 OK
{ "version": "2026-06-06", "workflow_tier": "full", "payload": { "create_rule": { "method": "POST", "path": "/v1/workflows/", "required_fields": ["name", "trigger", "writes"] } }, "condition_builder": { "supports_multiple": true, "combiner": "all", "field_source": "selected_trigger.context_fields" }, "enrichment": { "tasks": [{ "task": "extract_invoice", "output_schema": "invoice_v1" }], "schemas": [{ "schema": "invoice_v1", "fields": [...] }] }, "field_mapping": { "public_label": "Field mapping", "targets": [{ "target_type": "spreadsheet_row", "accepted_by": ["writes target file.spreadsheet.rows"] }] }, "review_policy": { "tenant_defaults": { "on_missing_required": "hold_for_review", "on_schema_mismatch": ["auto_remap", "hold_for_review"] }, "review_modes": ["auto", "hold_for_review", "delayed_auto_approve", "skip", "fail"] }, "triggers": [ { "name": "email_received", "scope": "inbox", "min_permission": "user", "context_fields": ["from_address", "from_domain", "subject"], "available": true } ], "condition_ops": [ { "op": "eq", "value_type": "scalar" }, { "op": "ai", "value_type": "string", "requires_llm": true } ], "write_targets": [ { "group": "Files", "description": "Extract source values, validate them against a target contract, then commit ready results.", "fields": [{ "name": "target.type", "widget": "select", "required": true, "source": { "method": "GET", "path": "/v1/labels/", "value_field": "label_id", "label_field": "name" } }], "available": true } ] }
Permission-restricted triggers are hidden. Plan-restricted triggers and write targets are returned with "available": false and "unavailable_reason": "requires_higher_workflow_tier". Write target fields define payload fields, widgets, defaults, required validation, fixed options, and picker source endpoints that builders should render.
For workflows that extract information before writing structured data, use writes. Gent derives the target fields, extracts values from the source, validates required fields, and either writes the result or holds it for review. Advanced reference output can include lower-level preparation phases for clients that need explicit chains.
Review policy controls what happens when data is missing, uncertain, shape-changed, or risky. Tenant defaults come from workspace settings. A workflow write may include a stricter override, but it cannot weaken the system safety floor or enable delayed auto-approval unless the tenant has allowed that behavior. When delayed auto-approval is allowed, held workflow writes are released by the scheduled approvals worker after the configured delay.
GET /v1/workflows/write-templates/ List reusable workflow writes  scope: workflows:read
[ { "template_id": "wrt_invoice_row", "name": "Append invoice row", "inbox_id": "<inbox-id>", "description": "Extract a file into the invoice register.", "source": {"from": "trigger.file"}, "target": {"type": "file.spreadsheet.rows", "file_id": "<sheet-id>"}, "policy": {"on_missing": "hold_for_review"} } ]
POST /v1/workflows/write-templates/ Create a reusable write → template_id  scope: workflows:write
Request
{ "name": "Append invoice row", "inbox_id": "<inbox-id>", "description": "Extract a file into the invoice register.", "source": {"from": "trigger.file"}, "target": {"type": "file.spreadsheet.rows", "file_id": "<sheet-id>"}, "policy": {"on_missing": "hold_for_review", "on_invalid": "hold_for_review"} }
FieldTypeRequiredDescription
namestringrequiredHuman-readable template name.
tenant_idstringoptionalUse for tenant-scoped reusable writes.
inbox_idstringoptionalUse when the write is tied to an inbox-scoped resource.
sourceobjectrequiredSame source object used in rule writes[].
targetobjectrequiredSame target object used in rule writes[].
contract, policy, review_policyobjectoptionalDefault contract and review behavior to reuse when a rule references this template.
201 Created
{ "template_id": "wrt_invoice_row", "name": "Append invoice row", "source": {"from": "trigger.file"}, "target": {"type": "file.spreadsheet.rows", "file_id": "<sheet-id>"} }
GET /v1/workflows/write-templates/{template_id}/ Get a reusable write  scope: workflows:read
{ "template_id": "wrt_invoice_row", "name": "Append invoice row", "source": {"from": "trigger.file"}, "target": {"type": "file.spreadsheet.rows", "file_id": "<sheet-id>"} }
PATCH /v1/workflows/write-templates/{template_id}/ Update a reusable write  scope: workflows:write
Request — all fields optional
{ "policy": {"on_missing": "hold_for_review", "on_invalid": "hold_for_review"} }
200 OK
{ "template_id": "wrt_invoice_row", "name": "Append invoice row", "source": {"from": "trigger.file"}, "target": {"type": "file.spreadsheet.rows", "file_id": "<sheet-id>"}, "policy": {"on_missing": "hold_for_review", "on_invalid": "hold_for_review"} }
DEL /v1/workflows/write-templates/{template_id}/ Delete a reusable write  scope: workflows:write
204 No Content
Deleting a write template does not edit existing workflow rules that already expanded it into their own writes.
POST /v1/workflows/recipes/validate/ Validate TOML recipe  scope: workflows:read
Request
{ "toml": "schema_version = \"2026-06-18\"\nkind = \"gent.workflow_recipe\"\n...", "inputs": { "inbox_id": "<inbox-id>", "lead_label_id": "<label-id>" } }
FieldTypeRequiredDescription
tomlstringrequiredWorkflow recipe TOML. Must use kind = "gent.workflow_recipe".
inputsobjectoptionalSetup values used to resolve {{ inputs.name }} placeholders for preview.
200 OK
{ "valid": true, "recipe_hash": "sha256:...", "name": "Lead follow-up", "inputs": { "inbox_id": {"type": "inbox", "required": true} }, "resources": { "sequence": [{ "key": "lead_followup", "payload": {...} }], "workflow": [{ "key": "lead_rule", "payload": {...} }] }, "diagnostics": [] }
This endpoint only validates and normalizes TOML recipes for preview. It does not create, update, delete, or execute anything. The dashboard installer uses the returned payloads with the existing sequence, work-route, and workflow-rule APIs after preview. See the recipe interface guide and TOML schema reference.
GET /v1/workflows/ List your rules
[ { "rule_id": "<rule-id>", "name": "tag-vip-emails", "created_by": "<user-id>", "trigger": "email_received", "trigger_scope": "inbox", "trigger_min_permission": "user", "inbox_id": "<inbox-id>", "tenant_id": null, "conditions": [], "writes": [], "enabled": true, "priority": 0, "created_at": "2026-05-22T10:00:00Z", "updated_at": "2026-05-22T10:00:00Z" } ]
POST /v1/workflows/ Create a rule → rule_id
Request
{ "name": "tag-vip-emails", "trigger": "email_received", "inbox_id": "you@example.com", "tenant_id": null, "conditions": [ { "field": "from_domain", "op": "eq", "value": "vip-client.com" } ], "writes": [ { "source": {"from": "trigger.email"}, "target": {"type": "email.add_label", "label_id": "<label-id>"} } ], "priority": 0 }
FieldTypeRequiredDescription
namestringrequiredUnique per creator, used as URL key.
triggerstringrequiredSee Triggers table below.
inbox_idstringrequiredRequired for inbox-scoped triggers.
tenant_idstringoptionalDefaults to the authenticated user's tenant for tenant-scoped triggers.
conditionsarrayoptionalAll conditions must match.
writesarrayrequiredOne or more source-to-target writes. Raw actions are not accepted.
201 Created
{ "rule_id": "<rule-id>", "name": "tag-vip-emails", "created_by": "<user-id>", "trigger": "email_received", "trigger_scope": "inbox", "trigger_min_permission": "user", "inbox_id": "<inbox-id>", "tenant_id": null, "conditions": [{ "field": "from_domain", "op": "eq", "value": "vip-client.com" }], "writes": [{ "source": {"from": "trigger.email"}, "target": {"type": "email.add_label", "label_id": "<label-id>"} }], "enabled": true, "priority": 0, "created_at": "2026-05-22T10:00:00Z", "updated_at": "2026-05-22T10:00:00Z" }
GET /v1/workflows/{rule_id}/ Get a rule by name
{ "rule_id": "<rule-id>", "name": "tag-vip-emails", "created_by": "<user-id>", "trigger": "email_received", "trigger_scope": "inbox", "trigger_min_permission": "user", "inbox_id": "<inbox-id>", "tenant_id": null, "conditions": [], "writes": [], "enabled": true, "priority": 0, "created_at": "2026-05-22T10:00:00Z", "updated_at": "2026-05-22T10:00:00Z" }
PATCH /v1/workflows/{rule_id}/ Update a rule
Request — all fields optional
{ "conditions": [ { "field": "from_domain", "op": "eq", "value": "vip-client.com" } ], "writes": [ { "source": {"from": "trigger.email"}, "target": {"type": "email.add_label", "label_id": "<label-id>"} } ], "enabled": true, "priority": 0 }
FieldTypeRequiredDescription
conditionsarrayoptionalReplaces existing.
writesarrayoptionalReplaces existing writes. Raw actions are not accepted.
prioritynumberoptional
200 OK
{ "rule_id": "<rule-id>", "name": "tag-vip-emails", "created_by": "<user-id>", "trigger": "email_received", "trigger_scope": "inbox", "trigger_min_permission": "user", "inbox_id": "<inbox-id>", "tenant_id": null, "conditions": [], "writes": [], "enabled": true, "priority": 0, "created_at": "2026-05-22T10:00:00Z", "updated_at": "2026-05-22T10:00:00Z" }
DEL /v1/workflows/{rule_id}/ Delete a rule
# 204 No Content — empty body
GET /v1/workflows/triggers/ List usable triggers  scope: workflows:read
{ "version": "2026-06-06", "resource": "workflow_triggers", "workflow_tier": "full", "triggers": [ { "name": "email_received", "scope": "inbox", "min_permission": "user", "context_fields": ["from_address", "from_domain", "subject"] } ], "counts": {"triggers": 1} }
GET /v1/workflows/conditions/ List condition operators  scope: workflows:read
{ "resource": "workflow_conditions", "conditions": [ {"op": "eq", "value_type": "scalar"}, {"op": "ai", "value_type": "string", "requires_llm": true} ], "counts": {"conditions": 6} }
GET /v1/workflows/write-targets/ List write target contracts  scope: workflows:read
{ "resource": "workflow_write_targets", "workflow_tier": "full", "write_targets": [ { "type": "send_webhook", "group": "Outbound", "available": true, "fields": [{"name": "url", "widget": "url", "required": true}] } ], "counts": {"write_targets": 1} }
POST /v1/workflows/{rule_id}/test/ Dry-run a rule  scope: workflows:read
Request — all optional
{ "message_id": "<message-id>" }
FieldTypeRequiredDescription
message_idstringoptionalTest against a specific message; omit to use the most recent.
200 OK
{ "rule_id": "<rule-id>", "trigger": "email_received", "matched": true, "context_used": { "from_address": "alice@example.com", "subject": "Hello", }, "conditions": [{ "field": "from_domain", "op": "eq", "value": "vip.com", "result": true }, { "op": "ai", "value": "The email sounds urgent", "result": true, "reasoning": "AI evaluated — see result" }], "writes": [{ "target": {"type": "email.add_label"}, "note": "would execute" }] }
No writes are executed. AI conditions are evaluated using the inbox's LLM config and count toward usage. Returns the context derived from the message so you can verify what fields the rule sees.
→ Workflow field reference
Triggers
triggerScopePermissionContext fields available in conditions & action templates
Inbox — email
email_receivedinboxuserfrom_address, from_name, from_domain, to_address, subject, has_attachments, body_preview, thread_id, sender_label_ids
email_sentinboxuserfrom_address, from_name, from_domain, to_address, subject, has_attachments, body_preview, thread_id
missed_replyinboxusermessage_id, to_address, days_since_sent, subject. Fires when a sent message has no reply after missed_reply_threshold_days.
message_bouncedinboxuserrecipient, bounce_category (hard·soft·spam_policy·unknown), smtp_reply
spam_complaint_receivedinboxuserrecipient. Fires when a recipient marks a sent message as spam.
label_appliedinboxuserlabel_id, label_name, message_id, thread_id, from_address, subject, body_preview. Fires when any label is applied to a message — use a condition to match a specific label. Can start sequences or work routes when the labeled email has sender context.
Inbox — contacts & threads
contact_createdinboxuseremail, email_domain
thread_assignedinboxuserthread_id, assigned_from, subject. Fires on the assignee inbox.
contact_unsubscribedinboxuseremail, label_id (broadcast label the contact opted out of)
calendar_event_createdinboxuserorganizer, organizer_domain, title
Inbox — files
file_createdinboxuserfile_id, name, file_type, folder_id, source_message_id. Fires when any file is created — use a condition to match by file_type or folder_id.
file_updatedinboxuserfile_id, name, file_type, folder_id. Fires on metadata changes.
file_version_addedinboxuserfile_id, name, file_type, version_number, source_message_id. Fires when a new content version is saved.
file_rows_appendedinboxuserfile_id, name, rows_added, source_message_id. Fires when rows are appended to a spreadsheet.
Tenant — admin only
invite_redeemedtenantadmininvite_batch_label, role, email, email_domain
quota_threshold_hittenantadmininbox_id, metric, value, limit
cap_releasedtenantadmininbox_id, cap_type
billing_eventtenantadminevent_type, amount
Condition operators
opBehaviourvalue
eqExact match (string, bool, or number)String / bool / number
containsSubstring matchString
inField value is one of a listArray of strings
includesField is a list and value is present in it — used with sender_label_idsString (single item to check)
anyAlways true — match every event regardless of field valueOmit
aiLLM evaluates whether the natural-language description is true for the trigger context. Fails closed — returns false on LLM errors or missing config. Counts toward LLM usage.Natural language description, e.g. "The email sounds urgent"
Write targets
targetAdditional fieldsNotes
writes[]"source": {"from": "trigger.file|trigger.email|trigger.attachment|trigger.embedded_image|event.payload"}, "target": {"type": "file.spreadsheet.rows", "file_id": "<spreadsheet-id>"} or any target from /v1/workflows/reference/; "policy" is optionalThe public write surface. Gent derives target fields, extracts values from the source, validates required fields, and either commits the result or holds it for review.
source.extract"task": "extract_invoice", optional "engine", "ocr_engine", and "config"Intermediary extraction inside a write. Use it when the source needs OCR or structured understanding before the target can be committed.
source.mapOptional explicit field overrides such as {"Amount": "total_amount"}Spreadsheet row workflows normally infer target fields from spreadsheet headers. Use map overrides only when inference needs help.
Contract targets
email.*email.add_label, email.move_to_folder, email.reply, email.draft, email.attach_file, and related email mutation targetsEmail side effects are selected through target.type, not separate action names.
file.*file.spreadsheet.rows, file.create, file.updateFiles targets validate access and target schema before committing writes.
contact.*contact.ensure, contact.label, contact.noteContact side effects use the triggering sender or reviewed contract values.
sequence.enroll, work_route, thread.assign, webhook, alert.notify, taskTarget-specific ref fields are listed in /v1/workflows/reference/.Clients should build UI from the reference endpoint instead of hard-coding old direct write names.
All conditions in a rule must match for the writes to fire. Rules are addressed by rule_id. GET /v1/workflows/reference/ is the source for workflow builders. The trigger, condition, and write-target helper lists expose smaller slices of the same contract. Each write target includes compatibility metadata, and the API rejects incompatible trigger/write combinations and invalid extracted-field chains before activation.
Auth
  • Agent tokenworkflows:read for GET, workflows:write for POST/PATCH/DELETE. Inbox-scoped: only inbox-level triggers available (email_received, email_sent, etc.).
  • Session token — access via inbox ownership. Tenant-scoped and system triggers require session auth.
  • Rules created by a token are owned by the inbox and visible to session auth on the same inbox.
Plan gate: Creating or updating workflows requires the workflow feature tier to be lite or higher. POST and PATCH return 403 Forbidden on plans without workflow access. Workflow count, allowed trigger scopes, and available write targets are also gated by tier — the list endpoint always returns 200 regardless of plan.
AI Workflows

Workflows can incorporate LLM calls through AI conditions that decide whether a rule should fire and through contract-backed extraction that prepares reviewed values for deterministic targets. These features require an active LLM config on the inbox — see Enrichment for setup.

Cost: Each AI condition evaluation and each LLM-backed contract extraction counts toward the inbox's LLM usage. Rules with AI conditions that match frequently will accumulate usage faster than deterministic rules.
Plan gate: AI conditions (op: "ai") require workflows:lite or higher. Contract extraction with LLMs requires workflows:full. LLM consent must be enabled on the tenant.
AI conditions

Use "op": "ai" as a condition to evaluate the trigger context against a natural-language description. The LLM sees all available context fields for the trigger and returns a boolean. Use this for matching on tone, intent, or content that can't be expressed as a field comparison.

BehaviourDetail
Fails closedIf the LLM is unavailable, misconfigured, or returns an error, the condition evaluates to false and the rule does not fire. This prevents mis-triggers from propagating when AI is degraded.
No field keyAI conditions don't filter on a single field — they receive the whole context. Omit "field" entirely.
Lengthvalue must be 10–1000 characters. Keep descriptions specific — vague descriptions produce inconsistent results.
Test endpointPOST /v1/workflows/{rule_id}/test/ evaluates AI conditions against a real message and returns each result with a "reasoning" field. AI calls made during testing count toward usage.
Example — AI condition only fires on urgent emails
{ "name": "urgent-flag", "trigger": "email_received", "inbox_id": "<inbox-id>", "conditions": [{ "op": "ai", "value": "The sender is expressing urgency, frustration, or a time-sensitive request" }], "writes": [{ "source": {"from": "trigger.email"}, "target": {"type": "email.add_label", "label_id": "<urgent-label-id>"} }] }
Contract-backed AI work

Use writes when a workflow should prepare values from an email, attachment, file, or event payload and then write to a deterministic target. Gent describes the target contract, extracts values, validates required fields, and either commits the result or holds it for review based on the workflow policy.

TargetWhat happens
email.reply / email.draftUses reviewed contract values to send a reply or save a draft. Use review policies for uncertain generated text.
taskExtracts task fields such as summary, description, and due date, then creates a task.
contact.noteAppends reviewed notes to the sender's contact record.
file.spreadsheet.rowsReads the spreadsheet headers, extracts matching values from the source, and appends a row when the contract is ready.
webhookDispatches a validated event payload through the inbox webhook path.
Review flow for uncertain output

If extracted values are missing, invalid, or below confidence thresholds, the workflow policy decides whether to hold, skip, fail, or auto-approve. Held workflow writes appear in Approvals with action_type: "workflow:write". Approving commits the reviewed contract result without re-running extraction.

Scenarios
1 — Create a follow-up task from meeting requests
{ "name": "meeting-followup-task", "trigger": "email_received", "inbox_id": "<inbox-id>", "conditions": [{ "op": "ai", "value": "The sender is requesting a meeting, call, or demo" }], "writes": [{ "source": { "from": "trigger.email" }, "target": { "type": "task" }, "policy": { "on_missing": "hold_for_review", "on_invalid": "hold_for_review" } }] }
2 — Append invoice emails or attachments to a spreadsheet
{ "name": "invoice-register", "trigger": "file_created", "inbox_id": "<inbox-id>", "conditions": [{ "field": "folder_id", "op": "eq", "value": "<invoices-folder-id>" }], "writes": [{ "source": { "from": "trigger.file" }, "target": { "type": "file.spreadsheet.rows", "file_id": "<sheet-id>" }, "contract": { "required": ["Vendor", "Amount"] }, "policy": { "on_missing": "hold_for_review", "on_invalid": "hold_for_review" } }] }
Writing good AI conditions and contracts
  • AI condition descriptions should describe a specific pattern, not a general topic. "Email about billing" is vague; "The sender is questioning a charge or requesting a refund" is precise.
  • Combine AI conditions with deterministic conditions to reduce LLM calls — e.g. check from_domain eq "example.com" first so the AI condition only runs on the relevant subset.
  • Use target contracts to describe the exact fields required before a side effect commits. Missing or invalid fields should normally hold for review.
  • For spreadsheet workflows, keep column headers clear and stable so Gent can map extracted values to the right row fields.
Sequences

Multi-email follow-up sequences that execute over time. Two modes: template sequences (reusable definitions started via workflow rules) and ad-hoc enrollments (custom per-recipient emails defined inline at enrollment time). Emails use either a static body or a prompt evaluated by the LLM at send time using the enrollment source context.

Starting from email: A workflow rule with trigger label_applied and action start_sequence can start a follow-up when a label is applied to an email. For custom per-conversation sequences, clients can call POST /v1/sequences/enrollments/ directly with inline emails and a shared source object. A reply from the contact automatically exits any active exit_on_reply enrollments.
Plan gate: Sequence enrollment via workflow rules requires workflows:full. Direct API enrollment requires sequences:write scope.
GET /v1/sequences/reference/ Sequence builder contract  scope: sequences:read
200 OK shape
{ "resource": "sequences", "sequence_fields": [...], "email_fields": [...], "recipient_fields": [...], "enrollment_fields": [...], "validation": {...} }
Use this contract to build sequence template and enrollment forms. It includes payload operations, field widgets, defaults, validation limits, statuses, and supported enrollment actions.
POST /v1/sequences/ Create a reusable sequence template  scope: sequences:write
Request
{ "name": "deal-follow-up", "exit_on_reply": true, "sender_inbox_id": "<inbox-id>", "emails": [ { "delay_days": 0, "prompt": "Acknowledge receipt, confirm follow-up deadline from thread." }, { "delay_days": 3, "prompt": "Follow up — reference what was asked and what's outstanding." }, { "delay_days": 7, "body": "Just checking in one last time — happy to jump on a call.", "subject": "Still here if you need me" } ] }
FieldTypeRequiredDescription
namestringrequiredHuman-readable name.
emailsarrayrequiredOrdered emails. Each email has delay_days (int), prompt (LLM instruction, evaluated at send time) or body (static text), and optional subject.
exit_on_replybooloptionalExit all active enrollments when the contact replies. Default true.
exit_condition_promptstringoptionalLLM instruction for evaluating whether a reply warrants exit (e.g. "exit if the reply indicates the request was fulfilled").
sender_inbox_idstringoptionalDefault sending inbox. Overridden at enrollment.
201 Created
{ "sequence_id": "<uuid>", "name": "deal-follow-up", "emails": [...], "...": "..." }
GET /v1/sequences/ List sequences for the tenant  scope: sequences:read
200 OK
[{ "sequence_id": "...", "name": "...", "emails": [...], "exit_on_reply": true, ... }]
GET /v1/sequences/{sequence_id}/ Get a sequence  scope: sequences:read
PATCH /v1/sequences/{sequence_id}/ Update a sequence  scope: sequences:write
Updatable fields: name, emails, exit_on_reply, exit_condition_prompt, sender_inbox_id. Active enrollments are unaffected — they copied emails at enrollment time.
DELETE /v1/sequences/{sequence_id}/ Delete a sequence  scope: sequences:write
Deletes the definition only. Active enrollments continue running — they carry their own copy of the emails.
POST /v1/sequences/enrollments/ Start a sequence enrollment  scope: sequences:write
Template enrollment — reference a saved sequence
{ "recipient": { "contact_id": "<contact-id>" }, "inbox_id": "<inbox-id>", "sequence_id": "<sequence-id>" }
Ad-hoc enrollment — custom emails for this recipient only
{ "recipient": { "email": "lead@example.com" }, "inbox_id": "<inbox-id>", "exit_on_reply": true, "source": { "type": "email", "message_id": "<message-id>", "thread_id": "<thread-id>" }, "emails": [ { "delay_days": 0, "prompt": "Acknowledge their specific concern about pricing and the Q3 numbers they mentioned." }, { "delay_days": 4, "prompt": "Follow up — they mentioned a board meeting on the 15th, reference that." } ] }
FieldTypeRequiredDescription
recipientobjectrequiredRecipient to enroll: {"contact_id": "..."} or {"email": "..."}. Email recipients create or reuse a contact.
inbox_idstringrequiredInbox to send from.
sequence_idstringconditionalReference a saved sequence. Either this or emails is required.
emailsarrayconditionalInline emails for ad-hoc enrollment. Either this or sequence_id is required.
sourceobjectoptionalSource captured at enrollment time — injected into each email prompt at send time.
exit_on_replybooloptionalExit when the contact replies. Default true.
201 Created
{ "enrollment_id": "<uuid>", "status": "active", "current_email": 0, "next_email_at": "...", "...": "..." }
GET /v1/sequences/enrollments/ List enrollments  scope: sequences:read
ParamTypeDescription
contact_idstringFilter to a specific contact.
statusstringFilter by status: active, completed, exited, cancelled.
GET /v1/sequences/enrollments/{enrollment_id}/ Get an enrollment  scope: sequences:read
PATCH /v1/sequences/enrollments/{enrollment_id}/ Cancel an enrollment  scope: sequences:write
Request
{ "action": "cancel" }
Cancels all remaining scheduled emails. The enrollment record is preserved with status: "cancelled".
Work Routes

Multi-recipient email requests for approvals, acknowledgements, and stakeholder input. A route is owned by a creator inbox, can run recipients in parallel or ordered sequence, stays visible in agenda while active, and records recipient responses as route events.

Email replies only: Recipients respond by replying to the routing email. The inbound parser records the detected action through POST /v1/work-routes/{route_id}/responses/ or the equivalent service method. Use email replies for approval, acknowledgement, and text-response routes.
Deferred capabilities: Route links, structured forms, upload responses, and document-signing flows are intentionally outside email-only v1. Use the email response model now; those richer response surfaces depend on fuller Files/forms support.
Plan gate: Work routing starts on the Startup tier and is enforced by workflows:full. Agent tokens need work_routes:read for reads and work_routes:write for writes.
GET /v1/work-routes/reference/ Work route builder contract  scope: work_routes:read
200 OK shape
{ "resource": "work_routes", "request_types": [{ "value": "approval", "actions": ["approve", "reject", "request_changes"] }], "routing_modes": [{ "value": "parallel", "completion_rules": ["required", "any", "optional"] }], "completion_rules": [...], "failure_modes": [...], "deferred_capabilities": [...] }
Use this contract to render route builders. Ordered routes only allow completion: "required". Parallel routes allow required, any, and optional.
POST /v1/work-routes/ Create a work route  scope: work_routes:write
Request — parallel approval
{ "creator_inbox_id": "<inbox-id>", "title": "Approve vendor quote", "description": "Reply APPROVE, REJECT, or CHANGES with notes.", "request_type": "approval", "routing_mode": "parallel", "completion": "required", "failure_mode": "block", "due_at": "2026-06-18T12:00:00Z", "start": true, "source": { "type": "email", "message_id": "<message-id>", "thread_id": "<thread-id>", "subject": "Vendor quote" }, "response_detection": { "phrases": { "approve": ["approve", "approved", "ship it"], "reject": ["reject", "hold"] } }, "recipients": [ { "kind": "contact", "contact_id": "<contact-id>", "reminder_sequence_id": "<sequence-id>" }, { "kind": "email", "email": "finance@example.com" } ] }
FieldTypeRequiredDescription
request_typestringrequiredapproval, text, or acknowledgement.
routing_modestringrequiredparallel activates all recipients at start. ordered activates one recipient at a time.
completionstringrequiredrequired, any, or optional. Ordered routes only support required.
response_detectionobjectoptionalAction-keyed reply phrases, such as phrases.approve and phrases.reject. Phrases are shown in the routing email and used before learned/global detection.
recipientsarrayrequiredEach recipient is a contact, email, or inbox. Ordered routes may include positive unique order values.
reminder_sequence_idstringoptionalAuto-enrolled when the recipient becomes active and has a contact_id. Email-only recipients are routed but reminder enrollment is skipped.
201 Created
{ "route_id": "<route-id>", "status": "active", "recipients": [{ "recipient_id": "<recipient-id>", "status": "active", "reminder_enrollment_status": "active" }] }
POST /v1/work-routes/{route_id}/responses/ Record a recipient response  scope: work_routes:write
Request
{ "recipient_id": "<recipient-id>", "action": "approve", "evidence": { "source": "email_reply", "message_id": "<message-id>", "confidence": 0.96, "matched_text": "approved" } }
Valid actions come from GET /v1/work-routes/reference/. Approval routes support approve, reject, and request_changes. Text routes use submitted; acknowledgement routes use acknowledged. Accepted responses are visible in route status and agenda; no recipient confirmation email is sent.
POST /v1/work-routes/response-phrases/ Confirm a learned reply phrase  tenant admin
Request
{ "request_type": "approval", "action": "approve", "phrase": "sige", "locale": "fil", "confidence": 0.94 }
FieldTypeRequiredDescription
request_typestringrequiredapproval, text, or acknowledgement.
actionstringrequiredMust be valid for the request type.
phrasestringrequiredThe short reply text to match deterministically in future email replies.
tenant_idstringoptionalSystem admins only. Tenant admins record phrases for their own tenant.
localestringoptionalLocale hint. Defaults to *.
confidencenumberoptional0 to 1 confidence score for the reviewed classification.
Use this after LLM or human review confirms an email-reply classification. Future matching checks tenant phrases before seeded phrases, so non-English approvals can become deterministic.
GET /v1/work-routes/ List routes for an inbox  scope: work_routes:read
ParamTypeDescription
inbox_idstringCreator inbox. Agent tokens derive this from the token inbox.
statusstringOptional route status filter.
GET /v1/work-routes/{route_id}/ Get a work route  scope: work_routes:read
{ "route_id": "<route-id>", "status": "active", "recipients": [...], "due_at": "2026-06-18T12:00:00Z" }
PATCH /v1/work-routes/{route_id}/ Update route metadata  scope: work_routes:write
Request — all fields optional
{ "title": "Approve revised vendor quote", "description": "Reply by Friday.", "due_at": "2026-06-19T12:00:00Z", "failure_mode": "block" }
Only metadata can be updated after creation. Recipient graph, routing mode, completion rule, and response type are fixed for the route.
POST /v1/work-routes/{route_id}/start/ Start a draft route  scope: work_routes:write
Only draft routes can be started. Starting activates all parallel recipients or the first ordered recipient, then enrolls configured contact-based reminder sequences.
POST /v1/work-routes/{route_id}/cancel/ Cancel a nonterminal route  scope: work_routes:write
Cancels pending and active recipients. Completed, blocked, cancelled, and expired routes cannot be cancelled again.
GET /v1/work-routes/{route_id}/events/ List route events  scope: work_routes:read
[{ "type": "route.started", "actor": "owner@example.com", "details": {}, "created_at": "..." }]