Skip to main content

Documentation Index

Fetch the complete documentation index at: https://docs.neoagent.io/llms.txt

Use this file to discover all available pages before exploring further.

A backfill applies a workflow to entities (tickets or time entries) that already exist in your PSA, instead of waiting for new ones to come in via callbacks. Use it to apply a newly-built or refined workflow to history without flooding your environment all at once.
A workflow is the what (triage, dispatch, time-entry update). A backfill is the when and how-much — it dispatches that same workflow against a frozen filter of historical entities, paced over execution windows you choose.

When to use a backfill

  • You just built a new triage workflow and want it to process the last 30 days of open tickets, not just newly-created ones.
  • You changed your dispatch rules and want to re-run the corrected logic over recent tickets.
  • You want to test a workflow on a small sample of historical tickets before enabling it for live callbacks.
A backfill doesn’t modify the target workflow — it reuses it. Each entity that matches the backfill’s filter gets dispatched into the target workflow as if a callback had just arrived for it.

How it works

A backfill creates a long-running orchestrator (a Temporal workflow) that:
  1. Queries your PSA for entities matching the backfill’s filter spec.
  2. Drops entities that already have a successful execution of the target workflow (so re-runs are idempotent).
  3. Within the configured execution window, signals child workflow executions in batches of max_in_flight.
  4. Tracks succeeded and failed counts via Temporal visibility queries.
The orchestrator lives across days — it sleeps between window hours and resumes at the next configured start. You can pause, resume, or cancel it at any time.

Setting one up

From the dashboard, go to Backfills → New backfill. The wizard has four steps:
1

Pick the target workflow

The workflow you want to apply to historical entities. Must be enabled.
2

Choose a filter

Either Reuse workflow filters (the backfill processes the same entities the workflow normally matches, optionally narrowed with extra rules) or Custom filters (the backfill defines its own entity scope, ignoring the workflow’s filters).See Filter tickets for the rule-builder reference — the same filter syntax used in workflow conditions.
3

Configure the execution window

Pick the days of the week and start/end times when the backfill is allowed to dispatch. Times are interpreted in the timezone you select (defaults to your browser’s timezone).
4

Review and create

The review step previews how many tickets currently match your filter (within a safe upper bound) so you know what you’re committing to before you click Create backfill.

Execution windows

Windows tell the orchestrator when it’s allowed to dispatch. Outside the window, the orchestrator stays alive but doesn’t fan out — the row’s state stays RUNNING and the detail page shows a “Waiting for window” sub-badge with the next opening time. Each day can have one or more time windows. Windows can span midnight: 22:00 → 06:00 means the window runs from 22:00 on the configured day through 06:00 the next morning.
{
  "monday":    [{"run_start_time": "22:00:00", "run_end_time": "06:00:00"}],
  "tuesday":   [{"run_start_time": "22:00:00", "run_end_time": "06:00:00"}],
  "wednesday": [{"run_start_time": "22:00:00", "run_end_time": "06:00:00"}],
  "thursday":  [{"run_start_time": "22:00:00", "run_end_time": "06:00:00"}],
  "friday":    [{"run_start_time": "22:00:00", "run_end_time": "06:00:00"}],
  "saturday":  [],
  "sunday":    []
}
The classic pattern for a triage backfill is weeknights 22:00 → 06:00 in your local timezone. The PSA is quiet, the backfill stays out of business-hours traffic, and you wake up to a fresh batch of processed tickets.

Filter spec

The filter spec defines which entities the backfill dispatches. Shape:
{
  "result_entity_type": "TICKET",
  "ticket_conditions": {
    "conditions": {
      "combinator": "and",
      "rules": [
        {"field": "queueID",    "operator": "=", "value": "Level I Support"},
        {"field": "createDate", "operator": ">", "value": "2026-04-01"}
      ]
    },
    "custom_field_conditions": null,
    "workload_conditions": null
  }
}
The conditions shape is the same FIND_ENTITIES filter used by the workflow editor — see Filter tickets. When you choose Reuse workflow filters, the wizard merges the workflow’s existing conditions with any extra rules you add and snapshots them into filter_spec at submit time. Later edits to the workflow don’t shift an in-flight backfill. In Custom filters mode, the workflow’s own filters are intentionally bypassed at dispatch time. The backfill is the filter authority — every entity matching your rules runs the workflow, even if the workflow’s own filter would normally reject it.

States

StateMeaning
RUNNINGOrchestrator alive. Dispatches when inside the execution window.
PAUSEDUser paused. Orchestrator stays alive but skips dispatch.
COMPLETEDAll matching entities have a successful execution.
CANCELLEDUser cancelled. Orchestrator stopped.
FAILEDUnrecoverable error in the orchestrator.
The detail page surfaces a secondary “Waiting for window” sub-state when the orchestrator is RUNNING but currently outside the configured window — useful when a customer creates a backfill at 10 AM for a 22:00 window and wonders why nothing’s happening.

Counts on the row

  • started — entities the orchestrator has dispatched (one Temporal workflow run per entity). Equals processed_count on the DB row.
  • succeeded — child workflow runs that completed successfully.
  • failed — child workflow runs that completed with failure.
  • in flightstarted − succeeded − failed. Children mid-run.
Succeeded and failed counts are reconciled from Temporal visibility on each orchestrator tick, so they may lag the “started” count by ~1 second.

Pause, resume, cancel

ActionEffect
PauseOrchestrator stops dispatching at the next tick. In-flight children continue to completion.
ResumeOrchestrator picks up from last_cursor (the last entity it dispatched) on the next tick.
CancelTerminal. Orchestrator stops, row state becomes CANCELLED. In-flight children still complete on their own.
Pause and resume are reversible — use them freely to throttle a backfill that’s making more noise than expected. Cancel is permanent; you’d create a new backfill to pick up where one left off.

Cost

Each child execution is billed at the target workflow’s normal credit cost — a backfill of 1,000 tickets through a triage agent at 2 credits per run costs 2,000 credits. Backfills don’t add a markup; they’re just a pacing mechanism for the existing workflow’s per-run cost. The wizard’s preview step shows the upper-bound credit cost (total_entities × credits_per_run) before you commit, so the cost ceiling is explicit at create time.

Concurrency control

max_in_flight (default 5) caps how many child workflows are running concurrently. The orchestrator checks the live RUNNING count for the target workflow (regardless of source) and only dispatches up to that cap.
The cap counts all running executions of the target workflow, not just backfill-driven ones. If the workflow has 3 live triggered executions running, your max_in_flight=5 backfill only gets 2 slots until those finish. This protects PSA rate limits.

API endpoints

The backfill API is internal — the dashboard uses it, but it’s not part of the public API contract. Endpoints live under /api/backfills.
MethodPathWhat it does
POST/api/backfillsCreate a backfill (or preview cost with ?preview=true).
POST/api/backfills/verifyPreflight: count how many entities the filter would match.
GET/api/backfillsList backfills for the caller’s tenant. Filter by state query param.
GET/api/backfills/{id}Get a single backfill row.
POST/api/backfills/{id}/pauseSignal pause.
POST/api/backfills/{id}/resumeSignal resume.
POST/api/backfills/{id}/cancelSignal cancel.

Create payload

{
  "target_workflow_id": 23653,
  "entity_type": "TICKET",
  "filter_spec": {
    "result_entity_type": "TICKET",
    "ticket_conditions": {
      "conditions": {
        "combinator": "and",
        "rules": [{"field": "queueID", "operator": "=", "value": "Level I Support"}]
      },
      "custom_field_conditions": null,
      "workload_conditions": null
    }
  },
  "daily_execution_windows": {
    "monday":    [{"run_start_time": "22:00:00", "run_end_time": "06:00:00"}],
    "tuesday":   [{"run_start_time": "22:00:00", "run_end_time": "06:00:00"}],
    "wednesday": [{"run_start_time": "22:00:00", "run_end_time": "06:00:00"}],
    "thursday":  [{"run_start_time": "22:00:00", "run_end_time": "06:00:00"}],
    "friday":    [{"run_start_time": "22:00:00", "run_end_time": "06:00:00"}],
    "saturday":  [],
    "sunday":    []
  },
  "timezone": "America/New_York",
  "max_in_flight": 5,
  "total_estimated": 100
}

Verify payload

Same shape as create, but only the fields needed for filter evaluation. Returns a safety classification (OK / WARNING / ERROR) and the matched entity count:
{
  "safety_level": "OK",
  "matching_entities": 87,
  "thresholds": {"warning_at": 5000, "error_at": 100000}
}
Run verify from the wizard’s review step automatically, or call it directly to validate a filter without committing.

Troubleshooting

The orchestrator is alive but waiting for the next execution window. The detail page shows a “Waiting for window” sub-badge with the next opening time. If you created the backfill at 10 AM for a 22:00 window, this is expected — nothing is broken.
A 409 Conflict from POST /api/backfills (or WorkflowAlreadyStartedError at the Temporal layer) means there’s already a RUNNING or PAUSED backfill for that target workflow in your tenant. Cancel or pause-and-cancel the existing one first.
Reconciliation queries Temporal visibility on each tick. There’s a ~1 second eventual-consistency lag — wait one tick and refresh. If counts stay at zero for more than ~30 seconds, the child workflows themselves may be hung; check the workflow’s Event History to see what they’re doing.
The orchestrator only marks COMPLETED when every matching entity has a successful execution. Entities with only a FAILED row keep getting re-dispatched on each tick (Temporal’s deterministic workflow_id deduplicates the actual run, but processed_count keeps bumping). Cancel and re-create the backfill, or wait for a future “retry failed” action.
max_in_flight caps total concurrent executions of the target workflow, not just backfill-driven ones. If live triggered traffic is consuming slots, your backfill shares the pool. This is intentional — it protects your PSA’s rate limits.

Limits and guardrails

  • Concurrency: 1–10 per backfill (max_in_flight).
  • Safety thresholds at verify time: OK ≤ 5,000 matching entities, WARNING ≤ 50,000, ERROR > 100,000. Backfills above the WARNING threshold can still be created; ERROR-level can’t.
  • Orchestrator lifetime: 7 days max. A backfill that hasn’t finished in 7 days will time out — usually a sign the filter is too wide for the window cadence.
  • Concurrent backfills per workflow: 1 active (RUNNING or PAUSED) per (client, target_workflow_id). Multiple completed/cancelled backfills for the same workflow are fine.