When to use this endpoint

Save the route and start a run first. Inspect all dates, proposed Jobs and archive impact. Pass the returned revision unchanged into publish with the same scheduling fields.

Send operation fields inside one JSON-encoded data object. When data is present it replaces top-level operation fields; otherwise the handler accepts the top-level request. String data is limited to 2 MB. Reserved transport/internal keys are removed. Malformed or non-array JSON becomes an empty input, so validate JSON before sending.

Authentication and permissions

Authenticate with a signed API key or logged-in CRM session; session requests follow dispatcher CSRF checks. API-key requests use the key owner’s identity and permissions.

Requires the Contracts entitlement (features & 64), features2.contractProgrammes and Contracts View. Also requires features2.contractProgrammePublishing.

All storage belongs to the authenticated tenant. Contract operations follow the actor’s recursive HR manager/team scope, or the configured TeamAccess Contract/Department policy. Client administrators have broad Contract scope; private Programme and route ownership still applies. Active assignees must be within the permitted team.

For newly created work, the configured CRM Job customer/Contract eligibility restriction is checked independently. Publication revalidates it before changing prior work; adopted reactive Jobs use their existing identity.

The tenant must have the Programme storage upgrade (foundation 803; independent route ownership 804 before planning). Requests never create or repair tables. A missing/incompatible storage failure can return an initialization/storage error or Contracts operation failed. Check the saved state before retrying.

Parameters

ParameterTypeRequiredDescription
run_idPositive integer or digit stringYesActive actor-owned run whose Programme is active.
user_idPositive integer or digit stringYesActive local assignee within your permitted team; use the workspace’s board user choices.
start_dateYYYY-MM-DDYesReal tenant-local calendar date. No future-only restriction.
visitsInteger-compatibleNoDefaults to 1; cast to integer and must be 1 or 2, within remaining run Visit count.
saturday, sunday, bank_holidays0/1 or booleanNoEach defaults to 0. Setting 1 includes those calendar dates; 0 skips them.

Example request

$result = FM_api('ContractProgramme_preview', [
	'data' => json_encode([
		'run_id' => 1,
		'user_id' => 3,
		'start_date' => '2026-11-09',
		'visits' => 1,
	]),
]);

Example response

{
  "ok": 1,
  "revision": "cac96aea5e059502978ddd24194792bd01e4daf2a3c6ebacfa103292fb385e88",
  "preview": {
    "run_id": 1,
    "programme_id": 1,
    "programme_name": "Summer",
    "user_id": 3,
    "user_name": "Operative",
    "route_id": 1,
    "route_revision": 1,
    "start_date": "2026-11-09",
    "saturday": 0,
    "sunday": 0,
    "bank_holidays": 0,
    "visits": [
      {
        "number": 1,
        "days": [
          {
            "number": 1,
            "date": "2026-11-09",
            "capacity_minutes": 480
          }
        ],
        "jobs": [
          {
            "item_id": 1,
            "contract_id": 1,
            "site_id": 2,
            "site_name": "Site",
            "contract_name": "Grounds",
            "manager_id": 2,
            "area_id": 1,
            "area": "100",
            "form_id": 1,
            "config_revision": 3,
            "day_number": 1,
            "capacity_minutes": 480,
            "position": 1,
            "date": "2026-11-09",
            "start_minutes": 540,
            "duration_minutes": 60,
            "job_id": 0,
            "existing": [],
            "tasks": [
              {
                "id": 1,
                "deleted": 0,
                "ddate": 0,
                "cdate": 1792490400,
                "last_edited": 1792490400,
                "contract_id": 1,
                "sector": "",
                "subject": "Mow",
                "details": "Mow grass",
                "position": 1,
                "active": 1,
                "task_type_id": 0
              }
            ],
            "checklist": {
              "version": 1,
              "template_id": "legacy1",
              "revision": "8a7285cf176746811fdedc68de9750e61b799b18c655bf8b23859cfeb599e7ad",
              "name": "Contract Checklist",
              "questions": [
                {
                  "id": "legacy1",
                  "text": "Mow — Mow grass",
                  "field_name": "Contract Checklist / Contract Checklist / Mow — Mow grass"
                }
              ]
            }
          }
        ],
        "start_date": "2026-11-09",
        "end_date": "2026-11-09"
      }
    ],
    "archive": [],
    "working_days": 1,
    "jobs": 1
  }
}

Example from the disposable regression fixture. Database-derived numeric fields can be numbers or strings depending on the database driver; explicitly cast IDs/counts remain integers.

Errors and unusual responses

ResponseCondition
{"error":"not logged in"}No authenticated tenant context is available. Invalid signed credentials can be rejected by the dispatcher before the handler runs.
{"error":"Programme Contracts is not enabled"}Workspace flag or Contracts entitlement is disabled.
{"error":"Permission denied"}Contracts View or the required action permission is unavailable.
{"error":"This Contracts capability is not enabled"}The operation’s additional feature flag is disabled.
{"error":"Save this user’s route before publishing"}No actor-owned route for the Programme/user.
{"error":"Selected Visits exceed this run. Start a new run to reuse the programme."}Count is invalid or remaining Visits exhausted.
{"error":"There is no eligible work in the selected Visits"}All proposed work was skipped or route had no eligible items.

Legacy application errors may be returned with HTTP 200. Check the decoded response for error, not only the HTTP status.

Behaviour and edge cases

  • Read-only: computes a SHA-256 revision of the complete proposed payload and performs no writes. Success returns {ok:1,revision,preview}.
  • preview contains run_id, programme_id, programme_name, user_id/name, route_id/revision, requested start_date, calendar flags, visits, archive, working_days and jobs count. Each Visit has number, days, jobs, start_date/end_date. Days have number/date/capacity_minutes. Proposed jobs include item/Contract/Site identity, names, manager/area, measured area or null, Form/config revision, day/position/date/start_minutes/duration, existing reactive Job details, tasks and checklist snapshot.
  • Starts at the next unpublished Visit number for this run/user; each route day consumes an eligible local calendar date, including empty days. Visits follow consecutively. Jobs start at work-day-start (fallback 09:00), sequentially by item duration; capacity is not an enforced limit.
  • Skipped weekends and saved holidays advance the date. First-Visit Contract overrides omit their earlier work. Reactive Jobs already adopted are skipped; one reactive Job appears only once even in a two-Visit preview.
  • Archive impact covers prior live incomplete work for this Programme/user across runs, not just the creator’s current run or the requested date range. Each affected Contract must remain in scope.
  • Active Service Job Forms, active linked Sites and active Contracts are checked. No eligible proposed work returns an error rather than a zero-job preview.
  • The preview does not require Jobs Add/Edit by itself; publish checks those permissions separately. Missing/invalid dates and non-0/1 calendar flags are rejected. Derived area values outside the supported range become null rather than measured zero.

See it in Fieldmotion

CRM1

No direct caller was found in CRM1 source. This workflow is implemented by the opted-in CRM2 Programme Contracts workspace.

CRM2

In Job Progress select Publish Jobs, choose User/run and dates, then Preview. Filter for ContractProgramme_preview. Changing inputs clears the UI preview and requires reviewing again.

Related endpoints