Schedules
Previewing, creating, reading, changing and deleting schedules through the partner API.
A schedule is a stored engagement template and a cadence. When an enabled schedule is due, it creates an engagement from the template and submits it. An occurrence skipped for the plan cap, or because the schedule's creator no longer exists, creates no engagement; see Caps and skips. Schedules describes how a schedule fires.
| Method | Path | Success |
|---|---|---|
POST |
/api/v1/schedules/preview |
200 and the next three run times |
POST |
/api/v1/schedules |
201 and the schedule |
GET |
/api/v1/schedules |
200 and a list of schedules, newest first |
GET |
/api/v1/schedules/{schedule_id} |
200 and the schedule |
PATCH |
/api/v1/schedules/{schedule_id} |
200 and the schedule |
DELETE |
/api/v1/schedules/{schedule_id} |
204, no body |
A schedule that belongs to another organization is answered
404 Schedule not found, the same as one that does not exist.
Cadence¶
A cadence is either a preset or a cron_expr, never both, evaluated in a
timezone. The presets are daily, weekly, monthly and quarterly; a
cron_expr is a five-field cron expression that passes the one-hour floor.
Cadence gives the expression each preset compiles
to and the rules for the floor.
Timezones and DST describes how the timezone, an
IANA name, decides when a schedule fires.
Previewing a cadence¶
POST /api/v1/schedules/preview validates a cadence and returns its next three
run times, in UTC. It stores nothing.
curl -X POST https://<pentr-host>/api/v1/schedules/preview \
-H "X-API-Key: $PENTR_API_KEY" -H "Content-Type: application/json" \
-d '{"cron_expr": "0 7 * * 1-5", "timezone": "Europe/Madrid"}'
{
"cron_expr": "0 7 * * 1-5",
"timezone": "Europe/Madrid",
"next_runs": [
"2026-09-24T05:00:00+00:00",
"2026-09-25T05:00:00+00:00",
"2026-09-28T05:00:00+00:00"
]
}
| Field | Required | Notes |
|---|---|---|
cron_expr |
one of the two | A five-field cron expression |
preset |
one of the two | daily, weekly, monthly or quarterly |
timezone |
no, default UTC |
An IANA timezone name |
For a preset, the response's cron_expr is the expression it compiles to.
Creating a schedule¶
{
"name": "Weekly external",
"engagement": {
"targets": [{"host": "example.com", "type": "domain"}],
"scope": {
"in_scope": ["app.example.com"],
"out_of_scope": ["mail.example.com"]
},
"rules": {
"max_duration_minutes": 120,
"allowed_actions": ["scan", "enumerate"],
"denied_actions": ["exploit", "post_exploit"]
},
"framework": "soc2"
},
"preset": "weekly",
"timezone": "Europe/Madrid"
}
| Field | Type | Required | Notes |
|---|---|---|---|
name |
string | yes | 1 to 255 characters |
engagement |
object | yes | An engagement body, as on Engagements |
cron_expr |
string | one of the two | As for preview |
preset |
string | one of the two | As for preview; stored as the expression it compiles to |
timezone |
string | no, default UTC |
An IANA timezone name |
enabled |
boolean | no, default true |
A disabled schedule does not fire |
The engagement body is validated as it is on create, and stored as the
schedule's template with credentials removed. Unlike an engagement,
the template keeps each target's type. The template's framework is
stored in the same 50-character column as an engagement's; a value one
character over the limit is answered with a plain-text 500.
The schedule object¶
{
"id": "2dc69146-0623-4448-89c6-32965161ab7f",
"name": "Weekly external",
"framework": "soc2",
"engagement": {
"targets": [{"host": "example.com", "type": "domain"}],
"scope": {
"in_scope": ["app.example.com"],
"out_of_scope": ["mail.example.com"]
},
"rules": {
"max_duration_minutes": 120,
"allowed_actions": ["scan", "enumerate"],
"denied_actions": ["exploit", "post_exploit"]
},
"framework": "soc2"
},
"cron_expr": "0 9 * * 1",
"timezone": "Europe/Madrid",
"next_run_at": "2026-09-28T07:00:00+00:00",
"last_run_at": null,
"enabled": true,
"last_skip_reason": null,
"created_at": "2026-09-23T09:42:31.285379+00:00",
"updated_at": "2026-09-23T09:42:31.285380+00:00"
}
| Field | Meaning |
|---|---|
framework |
The template's framework, copied at creation |
engagement |
The stored template |
cron_expr |
The expression; a preset is returned compiled, and the object has no preset field |
next_run_at |
The next time the schedule is due, in UTC |
last_run_at |
The most recent occurrence that created and queued an engagement; null until one has |
last_skip_reason |
Why the most recent due occurrence was skipped; cleared by the next successful fire |
The reasons an occurrence is skipped, and the plan cap behind one of them, are in Caps and skips.
Changing a schedule¶
PATCH /api/v1/schedules/{schedule_id} changes four things: name,
cron_expr or preset, timezone and enabled. A field that is left out or
sent as null is not changed.
{"cron_expr": "0 7 * * 1-5", "enabled": false}
The template cannot be changed. An engagement or framework field in a
PATCH body is ignored without an error, and the schedule keeps its template. To
change the template, delete the schedule and create a new one.
When the expression or the timezone changes, the next run time is recomputed
from the moment of the PATCH. A change to name or enabled leaves
next_run_at as it was.
The body's own fields are validated first: cron_expr against UTC if the
body has no timezone, timezone on its own if the body has no cadence. When
the expression or the timezone differs from the stored value, the resulting
pair is then checked together. If that pair fails —
for example a timezone whose daylight-saving change takes an existing
expression under the one-hour floor — the answer is a plain-text
500 Internal Server Error and nothing is saved. Previewing the pair first
with POST /api/v1/schedules/preview returns the same check as a 422.
Deleting a schedule¶
DELETE /api/v1/schedules/{schedule_id} deletes the schedule and answers
204. The engagements it created are kept. A second delete answers 404.
Validation errors¶
A cadence that fails validation on preview, create or PATCH is answered 422
with a list under detail:
{
"detail": [
{
"type": "value_error",
"loc": ["body"],
"msg": "Value error, schedule fires more often than the 1:00:00 floor (observed a 0:05:00 gap); use at least an hourly cadence",
"input": {"cron_expr": "*/5 * * * *"},
"ctx": {"error": {}}
}
]
}
The msg values the cadence checks produce:
msg |
Cause |
|---|---|
Value error, cron_expr or preset is required |
Neither was sent, on preview or create |
Value error, give cron_expr or preset, not both |
Both were sent |
Value error, unknown preset '…'; expected one of daily, weekly, monthly, quarterly |
An unknown preset |
Value error, unknown IANA timezone '…' |
An unknown timezone |
Value error, cron expression must have exactly 5 fields |
Too few or too many fields |
Value error, invalid cron expression '…': … |
A field out of range or unparseable |
Value error, schedule fires more often than the 1:00:00 floor (observed a … gap); use at least an hourly cadence |
The one-hour floor |
Other fields fail with the standard validation entries, for example
String should have at least 1 character for an empty name.
Last updated 2026-09-24