Platform

Platform overviewArchitectureWorkflow orchestrationGitOps configurationGovernance and AAAAI and MCPKnowledge and contextRuntime and executionEvidence and monitoring

Use cases

All use casesProduction incidentRelease preparationHotfix to productionSecurity scan triage
Why NopsAIIntegrationsSecurity

Resources

All resourcesAI agent governanceMCP governanceMCP securitySelf-hosted platforms
PricingGitHub

Company

How a run worksAboutContactBook a demo

Schedules API

Storing an intent to run a pipeline later, and testing it without waiting for the clock.

ReferenceDeveloperOperatorAutomation author

Key points

  • schedule_kind decides which timing field is read: recurring uses cron_expression, one-time uses run_at.
  • timezone is 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}/run answers 202 and does not move the next scheduled fire.

Operations

GET/v1/schedulesAuthorized

Lists schedules the caller can see.

Parameters

NameInTypeRequiredDescription
team_pathquerystringOptionalRestrict to schedules owned by one team path.
enabledquerybooleanOptionalRestrict to enabled or disabled schedules.

Call it

List schedulesapi-schedules request
curl -s -H "Authorization: Bearer $NOPSAI_TOKEN" "$NOPSAI_URL/v1/schedules" | jq '.[] | {identifier, cron_expression, enabled}'
Result

One entry per schedule, filtered to what the caller may see.

Responses

200application/json

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

StatusCauseWhat to do
400A filter value cannot be parsed.Check enabled is a boolean and team_path is a real path.
503Authorization is unavailable.Check AAA.
500The query failed.Platform fault.

Side effects

  • None.

Proven by

  • services/nopsai/schedules_test.go
  • services/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

Create a recurring scheduleapi-schedules request
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 .id
Result

The created schedule, including the identifier built from its path and name.

Replace before running
  • timezone is an IANA name. Without it, a cron expression means something different twice a year.

Responses

201application/json

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

StatusCauseWhat to do
400An invalid cron expression, an unknown timezone, a missing pipeline, or a one-time schedule without run_at.The message names the field that failed.
403The 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.
409A schedule with that path and name already exists.Update it instead, or pick another name.
500The schedule could not be stored.Retry.

Side effects

  • Registers the next fire time.
  • Writes an audit record.

Proven by

  • services/nopsai/schedules_test.go
  • services/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

NameInTypeRequiredDescription
scheduleIDpathuuidRequiredSchedule identifier.

Call it

Read a scheduleapi-schedules request
curl -s -H "Authorization: Bearer $NOPSAI_TOKEN" "$NOPSAI_URL/v1/schedules/$SCHEDULE_ID" | jq
Result

The schedule with its timing, ownership, variables, and the runs it recently produced.

Responses

200application/json

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

StatusCauseWhat to do
404No schedule with that id.Confirm the id from the list route.

Side effects

  • None.

Proven by

  • services/nopsai/schedules_test.go
  • services/nopsai/schedules.go
PUT/v1/schedules/{scheduleID}Authorized

Replaces a schedule definition.

Parameters

NameInTypeRequiredDescription
scheduleIDpathuuidRequiredSchedule to replace.

Call it

Replace a scheduleapi-schedules request
curl -sX PUT "$NOPSAI_URL/v1/schedules/$SCHEDULE_ID" \
  -H "Authorization: Bearer $NOPSAI_TOKEN" \
  -H "Content-Type: application/json" \
  --data @schedule.json | jq
Result

The stored schedule after replacement, with a recalculated next fire time.

Replace before running
  • A PUT replaces the whole document: omitted fields are cleared, not preserved.

Responses

200application/json

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

StatusCauseWhat to do
400Invalid timing, timezone, or pipeline reference.The message names the field.
403The caller may not schedule this pipeline or use its scope.Authorization is re-checked on every write.
409The new path and name collide with another schedule.Pick a different name.
500The update could not be persisted.Retry.

Side effects

  • Recalculates the next fire time.
  • Writes an audit record.

Proven by

  • services/nopsai/schedules_test.go
  • services/nopsai/schedules.go
PATCH/v1/schedules/{scheduleID}Authorized

Partially updates a schedule.

Parameters

NameInTypeRequiredDescription
scheduleIDpathuuidRequiredSchedule to update.

Call it

Move a schedule an hour laterapi-schedules request
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}'
Result

Only the supplied fields change; the rest of the schedule is untouched.

Responses

200application/json

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

StatusCauseWhat to do
400A supplied field is invalid.The message names the field.
403The caller may not modify this schedule.Check ownership of the team path.
500The 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.go
  • services/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

NameInTypeRequiredDescription
scheduleIDpathuuidRequiredSchedule to delete.

Call it

Delete a scheduleapi-schedules request
curl -sX DELETE -H "Authorization: Bearer $NOPSAI_TOKEN" "$NOPSAI_URL/v1/schedules/$SCHEDULE_ID" -w "%{http_code}\n"
Result

204. Runs it already produced stay in history.

Responses

204

Deleted.

When it fails

StatusCauseWhat to do
500The 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.go
  • services/nopsai/schedules.go
POST/v1/schedules/{scheduleID}/enableAuthorized

Enables a schedule.

Parameters

NameInTypeRequiredDescription
scheduleIDpathuuidRequiredSchedule to enable.

Call it

Enable a scheduleapi-schedules request
curl -sX POST -H "Authorization: Bearer $NOPSAI_TOKEN" "$NOPSAI_URL/v1/schedules/$SCHEDULE_ID/enable" | jq '{identifier, enabled}'
Result

The schedule with enabled: true and a next fire time.

Responses

200application/json

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

StatusCauseWhat to do
403The caller may not modify this schedule.Check team ownership.
404No schedule with that id.Confirm the id.
500The 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.go
  • services/nopsai/schedules.go
POST/v1/schedules/{scheduleID}/disableAuthorized

Disables a schedule without deleting it.

Parameters

NameInTypeRequiredDescription
scheduleIDpathuuidRequiredSchedule to disable.

Call it

Disable a scheduleapi-schedules request
curl -sX POST -H "Authorization: Bearer $NOPSAI_TOKEN" "$NOPSAI_URL/v1/schedules/$SCHEDULE_ID/disable" | jq '{identifier, enabled}'
Result

The schedule with enabled: false. It stops firing and keeps its definition and history.

Responses

200application/json

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

StatusCauseWhat to do
403The caller may not modify this schedule.Check team ownership.
404No schedule with that id.Confirm the id.
500The 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.go
  • services/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

NameInTypeRequiredDescription
scheduleIDpathuuidRequiredSchedule to run.

Call it

Fire a schedule immediatelyapi-schedules request
curl -sX POST -H "Authorization: Bearer $NOPSAI_TOKEN" "$NOPSAI_URL/v1/schedules/$SCHEDULE_ID/run" | jq
Result

202 with the run it queued, using the schedule’s scope, team path, and variables.

Responses

202application/json

A run was queued from the schedule.

{
  "run_id": "9c1f7a5e-2b44-4d2f-8f2a-2c9f0b6d4e11",
  "status": "queued"
}

When it fails

StatusCauseWhat to do
400The 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.
403The caller may not run this schedule.Check team ownership and scope access.
500The 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.go
  • services/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.go

    Schedule document, timing, and handlers.

  • services/nopsai/schedules_test.go

    Covering tests for schedule behaviour.