Operations

Recurring scans

Schedule a web scan daily, weekly or monthly at a local time in the console or from a terminal; how runs share the licensed slots, what happens when a run cannot start, the evidence export for auditors, the status endpoint for monitoring, and backup.

SectionOperations
UpdatedNovember 9, 2026
EditionSelf-hosted Enterprise, public beta

Gateway 0.2.0-beta.48 and later run scans on a schedule: one web target, daily (every day or chosen weekdays), weekly or monthly (a day from 1 to 28, or the last day), at a local time in a time zone you choose. Each run is an ordinary scan through the gateway: it takes a licensed slot like a scan someone starts, its results appear under Scans, and the audit log records it in the name of the schedule's owner. Schedules come with every licence.

A schedule belongs to a person, its owner. Each run is recorded, with its due time, when it started and finished, its outcome, the reason when it did not run, and the number of findings by severity. Those records stay when the scan itself is later deleted.

Before you start: authorize the target

A scheduled run never authorizes its target. It runs only while the target has a standing authorization that a person recorded (Targets, Authorize), whatever the deployment's target authorization setting says. The schedule form shows whether the target is authorized and offers Authorize now, which records your own authorization. If someone revokes the authorization, the next run is refused ("target not authorized") and nothing is sent to the engine; the run after a person authorizes the target again goes ahead. This holds for passive schedules too.

Create a schedule in the console

From the scanner, open the session menu (the round button in the bottom-right corner) and choose Schedule scans (a viewer's says Scheduled scans); in the console, choose Schedules in its menu. Then choose New schedule and pick the target from the form's list: the web targets, the 500 most recently changed (add a new one in Targets first). Behind the gateway the scanner's own Targets menu does not offer Schedule scans.

  • Scan profile and Active testing. Active testing runs the standard active families plus template, NoSQL and GraphQL checks; the engine checks the family set when you save.
  • Repeats, At and Time zone. Repeats is weekly, Every day, on chosen days of the week (all seven start ticked), or monthly. The time zone is your browser's unless you change it. When clocks go forward and the time does not exist that day, the run starts at the first valid moment after it; when clocks go back, the first of the two times counts.
  • May start up to. How long after its time a run may still start: 6 hours unless you change it, at least 30 minutes. A run that cannot start within it is recorded as missed. Use it as the maintenance window.
  • Longest run. A scan still running after this (12 hours unless you change it, 1 to 24 hours) is cancelled and recorded as timed out, so one stuck scan does not hold back the later runs.

The line under the form says it in words, for example "Thorough, active, every Monday 03:30 Europe/Berlin, may start until 09:30". Run now starts a run outside the schedule; it is admitted like any other run.

Every time the Schedules view shows for a schedule (its next run, its runs' due times, when it was changed or a run acknowledged) is in the schedule's own time zone, followed by the zone's name, whatever your browser's zone is. The evidence export is in UTC.

Edit changes what you change: the scan request is sent again only when you change the scan profile or Active testing. A schedule whose scan request was set from a terminal with a family set the form cannot show (an excluded family, for example) keeps that request: the form shows it read-only, and you change it with shakerscan api PATCH. Saving a schedule with its target also reads the target's address again, so after "the target's address changed" Edit and Save is the fix.

Signed-in (credentialed) schedules need engine 2.11.1 or later, whose scans test the credential profile's own sign-in endpoint at a lockout-safe pace. Name up to two credential profiles in the request (credential_profile_ids, from a terminal; the console form has no credential picker). Before each run the gateway checks that every one of them is active; an inactive, expired or deleted profile refuses the run ("credential") before anything is sent, and the status endpoint and the health card list profiles that were not active or due for renewal at their last run. On an older engine the gateway refuses to create one, and refuses its runs if the engine is moved back. Use the Balanced profile or a deeper one for a signed-in schedule: on Fast the engine may skip its web crawl to stay within the budget, and the scan can then sign in yet fetch no page behind the sign-in. To point the scan at the area behind the sign-in, name its pages as well (below, options.custom_endpoints, for example /hub/): the engine starts its discovery from them too, as it does for the known endpoints of a scan someone starts.

Who may do what

ViewerOperatorAdministrator
See schedules, their runs, the status and the evidence exportyesyesyes
Create, change, pause, resume, delete and run their own schedulesnoyesyes
Change anyone's schedule, reassign an ownernonoyes

A service token acts as the person it acts for (API access, Acts for) and changes only that person's schedules; a token that acts for nobody cannot own a schedule. Every change a token makes is written to the audit log with the marker person decision via token, the schedule's history names the person and the token's account, and the health card counts them. An administrator reassigns a schedule's owner with Reassign on its row (the runs are then sent as the new owner, and the change records who reassigned it).

Every change raises the schedule's revision. A change sent against an older revision is refused (409); reload and make it again. History, under Changes, lists each revision: when, what (created, edited, paused, resumed, reassigned, deleted), by whom and which settings changed.

From a terminal

shakerscan api sends these requests with your token:

shakerscan api GET /_enterprise/schedules
shakerscan api POST /_enterprise/schedules '{"name":"shop weekly","target_id":"<target id>",
  "request":{"budget_profile":"thorough","policy":{"active_testing":true,
  "preset":"standard_active","include_families":["nuclei_active","nosqli","graphql"]}},
  "cadence":{"kind":"weekly","weekday":"mon"},"time":"03:30","timezone":"Europe/Berlin"}'
shakerscan api PATCH /_enterprise/schedules/<id> '{"base_revision":1,"time":"04:00"}'
shakerscan api POST /_enterprise/schedules/<id>/pause '{"base_revision":2}'
shakerscan api POST /_enterprise/schedules/<id>/resume '{"base_revision":3}'
shakerscan api POST /_enterprise/schedules/<id>/run
shakerscan api DELETE '/_enterprise/schedules/<id>?base_revision=4'

A schedule's request accepts budget_profile, under policy active_testing, preset, include_families and exclude_families, credential_profile_ids (engine 2.11.1 and later), and under options custom_endpoints: up to 20 pages the scan also starts from, each a path on the target (/hub/, GET /hub/) or a URL on the target's own scheme, host and port, without a query, ; parameters (such as ;jsessionid=) or . and .. segments, also percent-encoded; they are stored and sent as GET /path, so a schedule moved to another target scans the same paths there. Anything else is refused. A PATCH changes only the fields it names. For example, to start a signed-in schedule's scan from /hub/ as well:

shakerscan api PATCH /_enterprise/schedules/<id> '{"base_revision":5,"request":{
  "budget_profile":"balanced","policy":{"active_testing":false,"preset":"passive"},
  "credential_profile_ids":["<credential profile id>"],"options":{"custom_endpoints":["/hub/"]}}}'

A PATCH that names request replaces the whole request, so it repeats the parts to keep. The console shows such a request read-only (the form has no field for pages or credentials).

How runs share the licensed slots

  • A slot stays for people. With two or more licensed slots, scheduled runs never take the last one: at most one fewer than the licensed slots run at once. When the engine has fewer scan workers than licensed slots (install sizes the workers to the host's memory), it is one fewer than the workers, since a free slot is no use to a person while every worker is busy. When the engine does not report its workers, the licensed slots alone decide, and Schedule settings says the engine has not said. An administrator can change that number under Schedule settings (raised to the licensed slots, scheduled runs can take them all). With one slot, a person waits for at most one scheduled scan. With one scan worker none can be left for people: a person's scan is admitted but waits while a scheduled scan runs, and Schedule settings and the licence card say so; add host memory and upgrade to get a second worker.
  • Waiting. A run that cannot start (slots busy, its schedule's previous run or another scan of the target still running, the engine not answering, an update being applied) waits and is tried again every 30 seconds, oldest first, until its window closes; then it is recorded as missed, with the reason. The console shows how many runs are queued.
  • Downtime. After the gateway was stopped, the latest run that fell due while it was down starts if its window is still open; every other one is recorded as missed (the gateway was not running).
  • Clock changes. If the host's clock jumps forward while the gateway runs, the due times it jumped over are recorded as missed (the gateway's clock stepped forward) and none of them starts; if the clock was wrong and is set back, those records are removed again and the schedules run at their real times. A virtual machine that its hypervisor paused or suspended, then resumed with the clock set forward, counts as a clock jump: a run that fell due meanwhile is recorded as missed (clock stepped forward) even if its window is still open, and the status endpoint shows it. A machine whose own operating system slept is not a clock jump: it counts as downtime (above).
  • Paused schedules record their due times as missed (paused). A schedule whose owner can no longer run scans (disabled, now a viewer, or a local account once single sign-on is required) is paused, and its run is refused; an administrator reassigns it (Reassign), then resumes it.
  • An unknown outcome (the engine did not answer while the run was being sent) is retried under the same request key, so the engine never starts it twice. If the outcome is still unknown after about 20 minutes, or the schedule was paused or deleted meanwhile, the run is never sent again: it says needs administrator and keeps its licensed slot. Check the engine's Scans list, then resolve the run in its history (Schedules, History): Resolve: the scan started with the scan's id (the run then follows that scan, and its slot is that scan's) or Resolve: I found no scan (its slot is released). The licence card lists the run's reservation with the others the gateway could not match, but sends you to the same place: Release slot does not settle a scheduled run (it would free the slot and leave the run counted against the scheduled-run limit, so later runs kept waiting).

Evidence for auditors

Export (CSV) under Schedules, or from a terminal:

shakerscan api GET '/_enterprise/schedules/evidence?from=2026-07-01&to=2026-09-30&format=json'

Every expected run in the period, with its schedule, due time, start and finish, outcome and reason, the scan, the scan profile and whether it was active, and the findings by severity (counted from the scan's own report as it ended, so a later scan of the same target does not change them). Without from and to the period is the last 90 days (records are kept 400 days by default). The header names the organization, the installation, the licence, who generated it and when. Times are UTC: ISO-8601 in the CSV, seconds since 1970 in the JSON rows (its header gives the period in ISO-8601 too). The due time, the end of the window and the admission are the gateway's clock; the start and the finish are the engine's, the scan's own start and end. Where the engine reports none (a scan deleted before the gateway read it, a run settled without a scan), they are when the gateway recorded the change. The header's clocks line says so. JSON comes in pages of 5,000 runs: follow next (pass it back as cursor, with or without the same from and to); a cursor is valid for the person it was issued to, for its period, until the gateway restarts. Each export carries a SHA-256 of its rows (CSV: on its last line), and the audit log records the export with the same digest. A person can start ten exports in five minutes.

The records are supporting evidence for PCI DSS 4.0 6.2.4, SOC 2 CC7.1 and ISO 27001:2022 A.8.8. Recurring scans are not an ASV scan, a WAF or a penetration test. Your auditor or QSA decides.

Monitoring

GET /_enterprise/schedules/status (any role, viewer tokens too) returns the failed, missed and needs-administrator runs of the last 7 days, the paused schedules, credential profiles of signed-in schedules that were not active or due for renewal at their last run, the number of changes made by tokens, and per schedule the high and critical findings of its latest run with the change since the run before. It answers 503 when something needs attention: with ?fail_on=runs (the default) for failed, missed or needs-administrator runs, with ?fail_on=runs,findings for high or critical findings as well. Otherwise it answers 200 ("attention": false). Due times a person stopped on purpose (the schedule paused or deleted) are in the evidence export, not in missed_7d, and do not turn it red; paused schedules are listed under paused. Point your monitoring at it with a viewer token.

Once a failed, refused or missed run is dealt with, an administrator acknowledges it with Acknowledge on its row in Schedules, History (or POST /_enterprise/schedules/runs/<run id>/acknowledge, from an administrator's browser session). The status endpoint and the health card then stop counting it (acknowledged_7d says how many were acknowledged), so one missed or cancelled run does not keep a monitor red for 7 days. The evidence export keeps the run as it was, with who acknowledged it and when, and the audit log records it. A run that needs an administrator is not acknowledged but resolved (above), which also settles its licensed slot.

The health card shows the same: runs that failed, were missed or need an administrator, paused schedules, credentials to renew, changes made by tokens, and single sign-on owners who have not signed in for 60 days (without SCIM their role is only updated when they sign in).

Backup, restore and upgrade

The schedules and their runs' records are a file of their own on the gateway's state volume, workflow/workflow.sqlite3 (in a directory of its own: earlier gateways refuse a second database file beside enterprise.sqlite3). backup.sh writes it to the backup as workflow.sqlite3, before the engine database, so every scan a run names is in the database dump. It is not encrypted (like the database dump, it holds ids, names and outcomes, never a credential); keep the backup directory encrypted at rest. When the gateway cannot export it (a damaged file, for example), the backup leaves it out, says so, records the reason in its MANIFEST and takes everything else.

restore.sh checks it and restores it with the identity state; a damaged file or one from another installation is refused before anything is stopped. While the restore runs, the copy it hands the gateway is readable only by the gateway's user, never by other local users. A deployment whose gateway image is older than 0.2.0-beta.48 cannot restore schedules: the restore then refuses before the stop, unless you run it with --without-schedules. A backup without the file restores with no schedules. A run whose outcome was unknown when the backup was taken is retried only inside its window; otherwise it is recorded as unknown (restored).

Gateway updates. The console's gateway update takes a snapshot of the schedules with the identity state, and a rolled-back update puts both back. Runs recorded during the update are kept (none is sent twice), and so are acknowledgements made during it; schedule changes made during it are undone, like other changes made then. A schedule's revision never goes back: the undoing is a revision of its own, after the ones made during the update (which stay in its history), so a change sent against one of those is refused as stale. A schedule created during the update is kept as deleted, with its history, and a run recorded during the update that has not started yet is sent as the schedule now says. What no rollback can undo is kept as well: the licensed slots of scans the engine still runs, the monthly usage counts, and the audit log's records of what happened during the update; the audit log then records the rollback itself (state.rollback). This holds for a rollback to gateway 0.2.0-beta.48 or later; a rollback to an earlier gateway runs that gateway's own rollback.

Records are kept 400 days by default; an administrator can set at least 13 months under Schedule settings.

Restore the schedules alone

restore.sh restores a whole deployment into empty state. When only workflow.sqlite3 is damaged (the health card says recurring scans are off), restore that file from the latest backup on its own, with the gateway stopped:

docker compose -f <deployment>/compose.json stop enterprise
docker compose -f <deployment>/compose.json run --rm --no-deps -T --entrypoint sh enterprise \
  -c 'cd /data/enterprise/workflow && for f in workflow.sqlite3 workflow.sqlite3-wal workflow.sqlite3-shm; do [ ! -e "$f" ] || mv "$f" "damaged-$f"; done'
docker compose -f <deployment>/compose.json run --rm --no-deps -T --entrypoint python enterprise \
  -m shakerscan_saas.enterprise.cli workflow restore --input - < <backup>/workflow.sqlite3
docker compose -f <deployment>/compose.json up -d enterprise

The restore is create-only (it never replaces a file in place), checks the copy belongs to this installation and is whole, and records runs that can no longer be retried as unknown (restored). Runs made since that backup are not in it. When the health card says the file was written by a newer gateway release, upgrade to that release again, or put back the snapshot taken before its migration (workflow/workflow.sqlite3.pre-v<N>-<time>.bak) the same way.

Moving from the host timer. If you used the Scheduled scans timer recipe, disable its timers and revoke its token when you create the console schedule; running both scans the target twice.