Webhooks and automations

Automations are server-side rules that run with nobody clicking: a trigger and a list of steps, stored as JSON, versioned and dry-runnable. They are how data comes in from other systems and goes out to them.

Anatomy of a rule

A rule is JSON: name, enabled, trigger_type, trigger_config and action_spec.steps. Step values can use templates such as {{trigger.row.id}}, {{trigger.body.field}} and {{steps.0.row.id}}; templates are path lookups only, and arithmetic goes in a compute step. Rules run on a durable engine with at-least-once execution, every change to a rule is versioned and restorable, and a new rule saved through MCP is always switched off until someone turns it on.

Create, test and run

TaskMCP tool
Read the reference (triggers, steps, recipes)get_automation_docs
Check a rule without savingvalidate_automation
Create or replace a rule (saved off)save_automation
Dry-run against a real record: nothing written or sentsimulate_automation
Turn on or offset_automation_enabled
Run now, for real (after confirm)run_automation
Runs, failures, retries, versionslist_automation_runs, list_failed_runs, retry_automation_run, restore_automation_version
Human approval stepslist_pending_approvals, decide_approval

The same operations are available on the automation API to a signed-in administrator session, and in the web app.

Trigger types

TriggerFires when
entity_eventA row is created, updated or deleted, optionally filtered by a condition before the run starts.
status_transitionA field changes from one value to another, once, with the old and new value.
cronOn a 5-field cron schedule in a time zone.
manualOn demand, including from an app button.
webhookAn external system POSTs to the rule's URL.
beaconAn email open-tracking pixel is loaded.
field_thresholdA number crosses a threshold.
time_in_stateA row has sat in one state too long (SLA checks).
date_relativeA set time before or after a date column.
recurring_from_dateEvery N days, weeks or months after a date column.
zone_membershipA record enters or leaves a service zone.
record_proximityA tracked person arrives at or leaves a record's location.
device_silenceA tracked phone goes quiet.
schedule_conflictBookings overlap or someone exceeds a daily-hours cap.
aggregate_thresholdA roll-up value crosses a line.

Platform events also arrive as entity_event triggers: inbound email and SMS received, voice calls and text conversations completed, proposals accepted, declined, viewed or expired, and public forms opened, submitted, signed or paid.

entity_event with a condition
"trigger_type": "entity_event",
"trigger_config": {
  "table": "jobs", "event": "updated",
  "condition": [
    { "col": "priority", "op": "eq", "value": "urgent" },
    { "col": "status",   "op": "in", "value": ["new", "assigned"] }
  ]
}

Inbound webhooks

  • URL: POST https://<api-host>/automation/hooks/<workspace-id>/<rule-id>. No session sign-in.
  • If the rule has trigger_config.secret, send it in the x-automation-secret header; otherwise the call gets 401.
  • The rule must be switched on; otherwise the call gets 404.
  • The response is 202 {"accepted": true} and the rule runs in the background.
  • Steps read the JSON body as {{trigger.body.<field>}}.
rule
{
  "name": "Website request to service request",
  "enabled": true,
  "trigger_type": "webhook",
  "trigger_config": { "secret": "<your-shared-secret>" },
  "action_spec": {
    "steps": [
      { "type": "create_record", "table": "service_requests",
        "values": {
          "contact_name": "{{trigger.body.name}}",
          "phone": "{{trigger.body.phone}}",
          "description": "{{trigger.body.issue}}",
          "status": "new"
        } }
    ]
  }
}
curl
curl -X POST "https://<api-host>/automation/hooks/<workspace-id>/<rule-id>" \
  -H "Content-Type: application/json" \
  -H "x-automation-secret: <your-shared-secret>" \
  -d '{"name": "Dana Lee", "phone": "+15550100", "issue": "No cooling, unit 4B"}'

# => 202 {"accepted": true}   (the rule runs in the background)

Outbound: HTTP requests and signed webhooks

http_request calls any URL with a method, headers and a JSON body (or a file's bytes with body_from), with a timeout and retries. It returns { ok, status, body } and does not throw on a non-2xx response, so gate later steps on {{steps.N.ok}}. Credentials come from the workspace's encrypted secret vault as {{secrets.NAME}}; an unknown name stops the step rather than sending a blank credential, and secret values never reach run history.

http_request step
{ "type": "http_request",
  "url": "https://api.example.com/v1/work-orders",
  "method": "POST",
  "headers": { "authorization": "Bearer {{secrets.PARTNER_API_KEY}}" },
  "body": { "job_id": "{{trigger.row.id}}" },
  "timeout": 15000, "retries": 2 }

send_webhook also signs the body with HMAC-SHA256 and logs every attempt. It sends X-Fieldproxy-Signature: t=<unix>,v1=<hex> (the HMAC of `${t}.${body}`), X-Fieldproxy-Timestamp and a unique X-Fieldproxy-Delivery.

rule
{
  "name": "Tell our ERP when a job is completed",
  "enabled": true,
  "trigger_type": "status_transition",
  "trigger_config": { "table": "jobs", "field": "status", "to": "completed" },
  "action_spec": {
    "steps": [
      { "type": "send_webhook",
        "url": "https://erp.example.com/hooks/fieldproxy",
        "secret": "{{secrets.ERP_WEBHOOK_KEY}}",
        "body": { "event": "job.completed", "job_id": "{{trigger.row.id}}" } }
    ]
  }
}
verify on your side
// Node.js: verify a Fieldproxy send_webhook delivery
import crypto from "node:crypto"

function verify(rawBody, header, secret, toleranceSec = 300) {
  // header: "t=<unix>,v1=<hex>"
  const parts = Object.fromEntries(header.split(",").map((p) => p.split("=")))
  const expected = crypto.createHmac("sha256", secret)
    .update(`${parts.t}.${rawBody}`).digest("hex")
  const fresh = Math.abs(Date.now() / 1000 - Number(parts.t)) < toleranceSec
  return fresh && crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(parts.v1))
}

Step types

GroupSteps
Writecreate_record (insert or upsert), update_record (always needs a where), snapshot_record, end_tracking_session
Read and fan outquery, aggregate, assign_dispatch, geocode, find_available_slot, create_schedule_slot, geofence_check, optimize_route, for_each, branch
Controlcondition, rate_limit, dedupe, delay, wait_until, approval_gate, wait_for_event
Computecompute (math, dates and strings)
Messagingsend_email, send_sms, place_call, send_push, send_public_link, sign_request, collect_payment, post_to_chat, notify_chat, generate_pdf, generate_document
Outbound and fileshttp_request, send_webhook, fetch_file, upload_file, stamp_pdf, merge_pdf
AIai_extract, ai_generate, ai_classify, ai_summarise, translate
Integrations and syncapply_mapping, call_integration, pull_integration, db_sync, sql

Each step can set on_error to halt, continue, retry (up to 5 attempts) or catch.

send_sms step
{ "type": "send_sms", "to": "{{trigger.row.phone}}", "body": "Reminder: visit tomorrow 9am" }

Engine limits

  • At most 20 runs at once per workspace, with a 60-minute execution limit per run; a failed run is retried twice.
  • Rule chains stop after 5 hops, and a rule that fails 5 times in a row is switched off automatically.
  • A rule can nest steps up to 10 levels deep and 2,000 steps in all.
  • Sends follow the workspace's send rules: opted-out numbers are skipped and messages outside allowed hours are deferred, not dropped.

Checked against the Fieldproxy platform reference on October 18, 2026.

Get your own custom field service management within a day. Tailored to your workflows.

We set up a workspace on your own data and connect your AI or your systems on the call.

Get Demo