Recurring Jobs Walkthrough

TL;DR

Register a task once. Every weekly solve automatically includes it, and any vehicle pin chosen at registration carries forward to every future week with zero dispatcher intervention. This walkthrough shows the full lifecycle in five steps.

Enable first

Set RECURRING_ENABLED=true and restart the server. Without it, the /api/v1/recurring-jobs routes don't exist. The CRUD examples below use their own separate X-API-Key tenant resolution (see Authentication); the /optimize/sync call in Step 3 additionally needs the normal gateway X-API-Key + X-Client-Key pair, omitted here for brevity.

Step 1: Register a pattern with a vehicle pin

A client at site-A needs a weekly visit. We want the solver to pick the best team at registration time and remember that choice forever after (pin_after_first). The pattern has one occurrence.

curl -X POST https://api.fieldgenius.be/api/v1/recurring-jobs \
  -H "X-API-Key: fg-4412b9a6e7c64f1b9b2e3a5d8f6c1a02" \
  -H "Content-Type: application/json" \
  -d '{
    "external_id": "client-acme-weekly",
    "client_ref": "cust-acme",
    "recurrence": "weekly",
    "locations": [
      {"index": 0, "lat": 50.8487, "lon": 3.3458, "label": "depot"},
      {"index": 1, "lat": 51.1786, "lon": 4.4652, "label": "acme-site"}
    ],
    "resources": [
      {"id": "team-A", "depot_index": 0, "available_days": [0,1,2,3,4], "tw": {"start": 0, "end": 28800}},
      {"id": "team-B", "depot_index": 0, "available_days": [0,1,2,3,4], "tw": {"start": 0, "end": 28800}}
    ],
    "location_index": [1],
    "service_time":   [1800],
    "day":            [null], "day_rule":      ["free"],
    "tw":             [null], "tw_rule":       ["free"],
    "resource":       [null], "resource_rule": ["pin_after_first"],
    "mandatory":      [true]
  }'

The server runs a feasibility solve against the supplied fleet and resolves the pin_after_first knob immediately. The response always comes back with resource_rule: "fix" — the pattern never stores an unresolved pin:

{
  "id": "d2e1f508-24fa-4cb4-965c-f4cf269ba062",
  "version": 1,
  "active": true,
  "items": [
    {
      "occurrence_index": 0,
      "location_index": 1, "lat": 51.1786, "lon": 4.4652, "location_label": "acme-site",
      "day": null, "day_rule": "free",
      "tw": null,  "tw_rule":  "free",
      "resource_id": "team-A", "resource_rule": "fix",
      "mandatory": true
    }
  ]
}

The solver chose team-A. That choice is now permanent — every future week will inject this task with allowed_resource_ids: ["team-A"].

Step 2: Preview the expansion before solving

Before committing to a full solve you can inspect exactly what jobs a period would produce. Nothing is stored and no solve runs.

curl -X POST https://api.fieldgenius.be/api/v1/recurring-jobs/expand \
  -H "X-API-Key: fg-4412b9a6e7c64f1b9b2e3a5d8f6c1a02" \
  -H "Content-Type: application/json" \
  -d '{"period_key": "2026-W26"}'
{
  "jobs": [
    {
      "id": "rec::client-acme-weekly::0::2026-W26",
      "location_index": 1,
      "service_time": 1800,
      "mandatory": true,
      "allowed_resource_ids": ["team-A"]
    }
  ]
}

The job id format is rec::{external_id}::{occurrence_index}::{period_key} — deterministic and unique per occurrence per week.

Step 3: Weekly solve with automatic injection

Add a single recurring block to your normal /optimize/sync request. Everything else stays exactly the same:

curl -X POST https://api.fieldgenius.be/api/v1/optimize/sync \
  -H "X-API-Key: fg-4412b9a6e7c64f1b9b2e3a5d8f6c1a02" \
  -H "Content-Type: application/json" \
  -d '{
    "recurring": {"period_key": "2026-W26", "requested_days": null},
    "travel": {"distance_matrix": [[0,95000],[95000,0]]},
    "resources": [
      {"id": "team-A", "depot_index": 0, "time_window": {"start": 0, "end": 28800}},
      {"id": "team-B", "depot_index": 0, "time_window": {"start": 0, "end": 28800}}
    ],
    "jobs": [
      {"id": "adhoc-1", "location_index": 1, "service_time": 1200}
    ],
    "options": {"time_limit_seconds": 10}
  }'

The recurring job is injected alongside adhoc-1. Because the pin forces allowed_resource_ids: ["team-A"], the solver assigns it to team-A regardless of what would be cheapest:

{
  "status": "optimal",
  "routes": [
    {
      "resource_id": "team-A",
      "activities": [
        {"type": "start", "location_index": 0, "departure_time": 0},
        {"type": "service", "job_id": "rec::client-acme-weekly::0::2026-W26",
         "location_index": 1, "arrival_time": 95000, "departure_time": 96800},
        {"type": "service", "job_id": "adhoc-1",
         "location_index": 1, "arrival_time": 96800, "departure_time": 98000},
        {"type": "end", "location_index": 0, "arrival_time": 193000}
      ]
    }
  ]
}

Step 4: Next week — pin carries over automatically

No re-registration needed. The pin lives in the pattern row, not tied to any specific week. Preview W27 to confirm:

curl -X POST https://api.fieldgenius.be/api/v1/recurring-jobs/expand \
  -H "X-API-Key: fg-4412b9a6e7c64f1b9b2e3a5d8f6c1a02" \
  -H "Content-Type: application/json" \
  -d '{"period_key": "2026-W27"}'
{
  "jobs": [
    {
      "id": "rec::client-acme-weekly::0::2026-W27",
      "location_index": 1,
      "service_time": 1800,
      "mandatory": true,
      "allowed_resource_ids": ["team-A"]
    }
  ]
}

Same pin, different period key — zero dispatcher intervention between weeks.

Step 5: Feasibility rejection at registration

A pattern is checked before it's saved. If the proposed constraints are impossible against the supplied fleet, the server returns 422 and nothing is persisted — there is no row to roll back.

curl -X POST https://api.fieldgenius.be/api/v1/recurring-jobs \
  -H "X-API-Key: fg-4412b9a6e7c64f1b9b2e3a5d8f6c1a02" \
  -H "Content-Type: application/json" \
  -d '{
    "external_id": "bad-pattern",
    "locations": [
      {"index": 0, "lat": 50.8487, "lon": 3.3458},
      {"index": 1, "lat": 51.1786, "lon": 4.4652}
    ],
    "resources": [
      {"id": "team-A", "depot_index": 0, "available_days": [0,1,2,3,4],
       "tw": {"start": 0, "end": 28800}}
    ],
    "location_index": [1],
    "service_time":   [1200],
    "day":      [null],               "day_rule":      ["free"],
    "tw":       [{"start": 90000, "end": 96000}], "tw_rule": ["fix"],
    "resource": [null],               "resource_rule": ["free"],
    "mandatory": [true]
  }'
{
  "detail": "Pattern is infeasible against the current active patterns
             (solver status: infeasible). Check fixed time windows,
             service times, and resource constraints."
}

A time window of 90000–96000 seconds (25:00–26:40) is beyond any resource's shift. The server caught this at registration rather than letting it silently fail every weekly solve.

Next steps
  • Recurring Jobs API — full CRUD reference including PATCH (partial update), DELETE, and the active/pause toggle.

FieldGenius VRP API documentation. Generated from the engineering source of truth (.tex docs and app/ source).