Skip to content

Automated rule runs

An automated rule applies an operation — enrolling people, assigning content, syncing group membership — to everyone matched by a saved report. Rules run automatically on the platform's periodic sweep, but you can also trigger an out-of-cycle run through the API. This guide covers creating a rule, the trigger-and-poll flow, plus the webhook events that let you skip polling entirely.

Every endpoint in this guide lives under /automated-rules/, so your token needs the automations scope family: automations:read to list rules and runs, automations:write to create, update, reorder or trigger them.

Create a rule

A rule is three pieces: a saved report (segment_id — who it applies to), an operation (parameters.operation — what it does to each matched person), and a name. Three operations can be created through the API today:

parameters.operation What it does Operation-specific fields
sync_group_from_segment Mirrors the saved report's membership into one group group_id
sync_group_template_from_segment Derives one or more group names per person from a template, and keeps each derived group in sync group_name_template, group_name_delimiter
assign_content_item_to_user Assigns a content item to every matched person content_item_public_id, assigner_id (optional — defaults to your API token's user), time_to_complete, time_to_complete_days_offset

Other rule kinds are not yet creatable

Rules built from the event- and track-enrollment operations (enroll_user_on_next_event_of_event_type and friends) are fully visible and runnable through this API, but can't be created or reconfigured here yet — build those in the PlusPlus app. The exact request/response schema for every operation, creatable or not, is in the API reference.

Sync one group from an HR-driven saved report:

curl -X POST https://acme.plusplus.app/api/v2/automated-rules/ \
  -H "Authorization: Bearer your_token_here" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Sync Engineering group from HR segment",
    "segment_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
    "parameters": {
      "operation": "sync_group_from_segment",
      "group_id": "550e8400-e29b-41d4-a716-446655440000"
    }
  }'

Or assign onboarding content to everyone the report matches, with a 14-day deadline:

curl -X POST https://acme.plusplus.app/api/v2/automated-rules/ \
  -H "Authorization: Bearer your_token_here" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Assign the security policy to new hires",
    "segment_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
    "parameters": {
      "operation": "assign_content_item_to_user",
      "content_item_public_id": "9b2d1c3e-5f6a-4b7c-8d9e-0f1a2b3c4d5e",
      "time_to_complete": "custom",
      "time_to_complete_days_offset": 14
    }
  }'

An unresolvable reference in the body — an unknown segment_id, group_id, or content_item_public_id — returns 400 (invalid_segment / invalid_parameters) and creates nothing.

Test it, then enable it

A new rule is disabled by default and is appended at the end of the execution order. That's deliberate: trigger an explicit run first (POST .../run/ works while disabled — see below), inspect the run's matched_users/affected_users, then flip it on for the periodic sweep:

curl -X PATCH https://acme.plusplus.app/api/v2/automated-rules/{public_id}/ \
  -H "Authorization: Bearer your_token_here" \
  -H "Content-Type: application/json" \
  -d '{"is_enabled": true}'

Flow at a glance

sequenceDiagram
    participant Your app
    participant PlusPlus API
    Your app->>PlusPlus API: POST /automated-rules/{public_id}/run/
    PlusPlus API-->>Your app: 201 { public_id, status: "pending" }
    loop until status is terminal
        Your app->>PlusPlus API: GET /automated-rules/{public_id}/runs/{run_id}/
        PlusPlus API-->>Your app: 200 { status, matched_users, affected_users }
    end
    PlusPlus API--)Your app: webhook automated_rule.run_completed / run_failed

1. Trigger a run

curl -X POST https://acme.plusplus.app/api/v2/automated-rules/550e8400-e29b-41d4-a716-446655440000/run/ \
  -H "Authorization: Bearer your_token_here"

Execution is asynchronous — the response is 201 Created immediately, with the run queued (status: "pending"):

{
  "public_id": "6f1a2b3c-4d5e-6f70-8192-a3b4c5d6e7f8",
  "rule": { "public_id": "550e8400-e29b-41d4-a716-446655440000", "name": "Auto-enroll new hires in onboarding" },
  "status": "pending",
  "started_at": null,
  "stopped_at": null,
  "matched_users": null,
  "affected_users": null,
  "created": "2026-07-10T10:30:45.123Z"
}

Hold on to the run's public_id — it's how you poll for the outcome. Triggering a rule that's already running, or that already has a run queued, returns 400 (already_running / already_queued) rather than queuing a second run.

2. Learn the outcome — poll or subscribe

Poll

curl https://acme.plusplus.app/api/v2/automated-rules/550e8400-e29b-41d4-a716-446655440000/runs/6f1a2b3c-4d5e-6f70-8192-a3b4c5d6e7f8/ \
  -H "Authorization: Bearer your_token_here"

status moves from pending → running → a terminal value: ok, warning (no people matched), partially_failed, or failed. matched_users and affected_users populate once the run starts. Poll every few seconds; most rules finish quickly, but a rule enrolling thousands of people can take longer.

Or subscribe to the webhook events

Rather than polling, subscribe to automated_rule.run_completed (ok/warning/partially_failed) and automated_rule.run_failed (failed). Both deliver the same shape as the run endpoint above as data.object, and fire for every run — whether you triggered it via POST .../run/ or it ran on the platform's periodic sweep. See the Webhooks section of the API reference for the full event schema and subscription setup.

Rule history

GET /automated-rules/{public_id}/runs/ returns a paginated list of a rule's past runs, most recent first — useful for auditing or reconciling after a partially_failed outcome.

Common pitfalls

  • Treating 201 as done. Like the bulk async pattern, a 201 on run/ means queued, not finished.
  • Retriggering a running rule. already_running/already_queued (400) are expected responses if you call run/ again before the previous run finishes — not a transient error to retry-loop on.
  • Assuming every rule is runnable through this API. Only rules built from operations this API supports (enrollment, content assignment, group sync) are visible here; a rule using an unsupported operation 404s.