Key points
schedule_kinddecides which timing field is read:recurringusescron_expression, one-time usesrun_at.timezoneis an IANA name. Without one, a cron expression means something different twice a year.- A schedule carries its own scope, team path, and variables, so a scheduled run is not the same as a manual one.
- Authorization is checked when the schedule is written, because it fires later without a caller present.
- Enabling schedules the next fire from now: missed occurrences are not replayed.
POST /{id}/runanswers 202 and does not move the next scheduled fire.
Operations
GET/v1/schedulesAuthorized
Lists schedules the caller can see.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
team_path | query | string | Optional | Restrict to schedules owned by one team path. |
enabled | query | boolean | Optional | Restrict to enabled or disabled schedules. |
Call it
curl -s -H "Authorization: Bearer $NOPSAI_TOKEN" "$NOPSAI_URL/v1/schedules" | jq '.[] | {identifier, cron_expression, enabled}'Responses
Schedules visible to the caller.
[{
"id": "0f4b9c21-8d33-4e77-8a10-3c6f9b2d5a44",
"path": "platform",
"name": "nightly-release-check",
"identifier": "platform/nightly-release-check",
"pipeline": "platform/release-service",
"schedule_kind": "recurring",
"cron_expression": "0 2 * * *",
"timezone": "Europe/Berlin",
"enabled": true,
"scope": "platform/production",
"run_team_path": "platform/payments",
"variables": { "RELEASE_CHANNEL": "stable" }
}]When it fails
| Status | Cause | What to do |
|---|---|---|
| 400 | A filter value cannot be parsed. | Check enabled is a boolean and team_path is a real path. |
| 503 | Authorization is unavailable. | Check AAA. |
| 500 | The query failed. | Platform fault. |
Side effects
- None.
Proven by
services/nopsai/schedules_test.goservices/nopsai/schedules.go
POST/v1/schedulesAuthorized
Creates a schedule.
Notes
schedule_kind decides which timing field is read: recurring uses cron_expression, one-time uses run_at.
Call it
curl -sX POST "$NOPSAI_URL/v1/schedules" \
-H "Authorization: Bearer $NOPSAI_TOKEN" \
-H "Content-Type: application/json" \
-d '{"path":"platform","name":"nightly-release-check","pipeline":"platform/release-service","schedule_kind":"recurring","cron_expression":"0 2 * * *","timezone":"Europe/Berlin","scope":"platform/production","enabled":true}' | jq -r .idResponses
Schedule created.
{
"id": "0f4b9c21-8d33-4e77-8a10-3c6f9b2d5a44",
"path": "platform",
"name": "nightly-release-check",
"identifier": "platform/nightly-release-check",
"pipeline": "platform/release-service",
"schedule_kind": "recurring",
"cron_expression": "0 2 * * *",
"timezone": "Europe/Berlin",
"enabled": true,
"scope": "platform/production",
"run_team_path": "platform/payments",
"variables": { "RELEASE_CHANNEL": "stable" }
}When it fails
| Status | Cause | What to do |
|---|---|---|
| 400 | An invalid cron expression, an unknown timezone, a missing pipeline, or a one-time schedule without run_at. | The message names the field that failed. |
| 403 | The caller may not schedule this pipeline, or may not use its scope. | A schedule runs later with the same authorization it was created under, so it is checked now. |
| 409 | A schedule with that path and name already exists. | Update it instead, or pick another name. |
| 500 | The schedule could not be stored. | Retry. |
Side effects
- Registers the next fire time.
- Writes an audit record.
Proven by
services/nopsai/schedules_test.goservices/nopsai/schedules.go
GET/v1/schedules/{scheduleID}Authorized
Reads one schedule, including its recent runs.
Notes
The attached run history is the fastest way to tell "never fired" from "fired and failed".
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
scheduleID | path | uuid | Required | Schedule identifier. |
Call it
curl -s -H "Authorization: Bearer $NOPSAI_TOKEN" "$NOPSAI_URL/v1/schedules/$SCHEDULE_ID" | jqResponses
The schedule.
{
"id": "0f4b9c21-8d33-4e77-8a10-3c6f9b2d5a44",
"path": "platform",
"name": "nightly-release-check",
"identifier": "platform/nightly-release-check",
"pipeline": "platform/release-service",
"schedule_kind": "recurring",
"cron_expression": "0 2 * * *",
"timezone": "Europe/Berlin",
"enabled": true,
"scope": "platform/production",
"run_team_path": "platform/payments",
"variables": { "RELEASE_CHANNEL": "stable" }
}When it fails
| Status | Cause | What to do |
|---|---|---|
| 404 | No schedule with that id. | Confirm the id from the list route. |
Side effects
- None.
Proven by
services/nopsai/schedules_test.goservices/nopsai/schedules.go
PUT/v1/schedules/{scheduleID}Authorized
Replaces a schedule definition.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
scheduleID | path | uuid | Required | Schedule to replace. |
Call it
curl -sX PUT "$NOPSAI_URL/v1/schedules/$SCHEDULE_ID" \
-H "Authorization: Bearer $NOPSAI_TOKEN" \
-H "Content-Type: application/json" \
--data @schedule.json | jqResponses
Schedule replaced.
{
"id": "0f4b9c21-8d33-4e77-8a10-3c6f9b2d5a44",
"path": "platform",
"name": "nightly-release-check",
"identifier": "platform/nightly-release-check",
"pipeline": "platform/release-service",
"schedule_kind": "recurring",
"cron_expression": "0 2 * * *",
"timezone": "Europe/Berlin",
"enabled": true,
"scope": "platform/production",
"run_team_path": "platform/payments",
"variables": { "RELEASE_CHANNEL": "stable" }
}When it fails
| Status | Cause | What to do |
|---|---|---|
| 400 | Invalid timing, timezone, or pipeline reference. | The message names the field. |
| 403 | The caller may not schedule this pipeline or use its scope. | Authorization is re-checked on every write. |
| 409 | The new path and name collide with another schedule. | Pick a different name. |
| 500 | The update could not be persisted. | Retry. |
Side effects
- Recalculates the next fire time.
- Writes an audit record.
Proven by
services/nopsai/schedules_test.goservices/nopsai/schedules.go
PATCH/v1/schedules/{scheduleID}Authorized
Partially updates a schedule.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
scheduleID | path | uuid | Required | Schedule to update. |
Call it
curl -sX PATCH "$NOPSAI_URL/v1/schedules/$SCHEDULE_ID" \
-H "Authorization: Bearer $NOPSAI_TOKEN" \
-H "Content-Type: application/json" \
-d '{"cron_expression":"0 3 * * *"}' | jq '{identifier, cron_expression}'Responses
Schedule updated.
{
"id": "0f4b9c21-8d33-4e77-8a10-3c6f9b2d5a44",
"path": "platform",
"name": "nightly-release-check",
"identifier": "platform/nightly-release-check",
"pipeline": "platform/release-service",
"schedule_kind": "recurring",
"cron_expression": "0 2 * * *",
"timezone": "Europe/Berlin",
"enabled": true,
"scope": "platform/production",
"run_team_path": "platform/payments",
"variables": { "RELEASE_CHANNEL": "stable" }
}When it fails
| Status | Cause | What to do |
|---|---|---|
| 400 | A supplied field is invalid. | The message names the field. |
| 403 | The caller may not modify this schedule. | Check ownership of the team path. |
| 500 | The update could not be persisted. | Retry. |
Side effects
- Recalculates the next fire time when timing changed.
- Writes an audit record.
Proven by
services/nopsai/schedules_test.goservices/nopsai/schedules.go
DELETE/v1/schedules/{scheduleID}Authorized
Deletes a schedule.
Notes
Disabling is usually better than deleting while investigating: it stops the fires and keeps the definition and its history.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
scheduleID | path | uuid | Required | Schedule to delete. |
Call it
curl -sX DELETE -H "Authorization: Bearer $NOPSAI_TOKEN" "$NOPSAI_URL/v1/schedules/$SCHEDULE_ID" -w "%{http_code}\n"Responses
Deleted.
When it fails
| Status | Cause | What to do |
|---|---|---|
| 500 | The delete could not be persisted. | Retry. |
Side effects
- Stops future fires. Existing runs are untouched.
- Writes an audit record.
Proven by
services/nopsai/schedules_test.goservices/nopsai/schedules.go
POST/v1/schedules/{scheduleID}/enableAuthorized
Enables a schedule.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
scheduleID | path | uuid | Required | Schedule to enable. |
Call it
curl -sX POST -H "Authorization: Bearer $NOPSAI_TOKEN" "$NOPSAI_URL/v1/schedules/$SCHEDULE_ID/enable" | jq '{identifier, enabled}'Responses
Schedule enabled.
{
"id": "0f4b9c21-8d33-4e77-8a10-3c6f9b2d5a44",
"path": "platform",
"name": "nightly-release-check",
"identifier": "platform/nightly-release-check",
"pipeline": "platform/release-service",
"schedule_kind": "recurring",
"cron_expression": "0 2 * * *",
"timezone": "Europe/Berlin",
"enabled": true,
"scope": "platform/production",
"run_team_path": "platform/payments",
"variables": { "RELEASE_CHANNEL": "stable" }
}When it fails
| Status | Cause | What to do |
|---|---|---|
| 403 | The caller may not modify this schedule. | Check team ownership. |
| 404 | No schedule with that id. | Confirm the id. |
| 500 | The change could not be persisted. | Retry. |
Side effects
- Schedules the next fire from now, not from when it was disabled: missed occurrences are not replayed.
- Writes an audit record.
Proven by
services/nopsai/schedules_test.goservices/nopsai/schedules.go
POST/v1/schedules/{scheduleID}/disableAuthorized
Disables a schedule without deleting it.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
scheduleID | path | uuid | Required | Schedule to disable. |
Call it
curl -sX POST -H "Authorization: Bearer $NOPSAI_TOKEN" "$NOPSAI_URL/v1/schedules/$SCHEDULE_ID/disable" | jq '{identifier, enabled}'Responses
Schedule disabled.
{
"id": "0f4b9c21-8d33-4e77-8a10-3c6f9b2d5a44",
"path": "platform",
"name": "nightly-release-check",
"identifier": "platform/nightly-release-check",
"pipeline": "platform/release-service",
"schedule_kind": "recurring",
"cron_expression": "0 2 * * *",
"timezone": "Europe/Berlin",
"enabled": true,
"scope": "platform/production",
"run_team_path": "platform/payments",
"variables": { "RELEASE_CHANNEL": "stable" }
}When it fails
| Status | Cause | What to do |
|---|---|---|
| 403 | The caller may not modify this schedule. | Check team ownership. |
| 404 | No schedule with that id. | Confirm the id. |
| 500 | The change could not be persisted. | Retry. |
Side effects
- Stops future fires. A run already in flight is not cancelled.
- Writes an audit record.
Proven by
services/nopsai/schedules_test.goservices/nopsai/schedules.go
POST/v1/schedules/{scheduleID}/runAuthorized
Runs a schedule now, without waiting for its next fire.
Notes
Running now is the cheapest way to test a schedule’s scope and variables without waiting for 2am.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
scheduleID | path | uuid | Required | Schedule to run. |
Call it
curl -sX POST -H "Authorization: Bearer $NOPSAI_TOKEN" "$NOPSAI_URL/v1/schedules/$SCHEDULE_ID/run" | jqResponses
A run was queued from the schedule.
{
"run_id": "9c1f7a5e-2b44-4d2f-8f2a-2c9f0b6d4e11",
"status": "queued"
}When it fails
| Status | Cause | What to do |
|---|---|---|
| 400 | The id is malformed, or the schedule cannot produce a run — a missing pipeline, for instance. | Read the schedule first: the same reason would fail its next scheduled fire. |
| 403 | The caller may not run this schedule. | Check team ownership and scope access. |
| 500 | The run could not be created. | Retry. |
Side effects
- Creates a run as if the schedule had fired.
- Does not change the next scheduled fire time.
- Writes an audit record.
Proven by
services/nopsai/schedules_test.goservices/nopsai/schedules.go
How it works
Disable rather than delete while investigating. Disabling stops the fires and keeps the definition and its run history, which is exactly what you need to work out why the 2am run failed.
The run history attached to schedule detail separates the two failure shapes that look identical from the outside: a schedule that never fired, and one that fired and produced failing runs.
Because a schedule runs unattended, everything it needs must resolve without a caller: the scope must hold its variables and secrets, and the identity that created it must have been allowed to use them.
Implementation evidence
services/nopsai/schedules.goSchedule document, timing, and handlers.
services/nopsai/schedules_test.goCovering tests for schedule behaviour.

