Before you start
You need a completed scan of a tagged image —alpine:3.19, not
alpine@sha256:…. Why that matters is step 2. The
Quickstart produces exactly the right
thing.
1. Find the asset
A schedule belongs to an asset, not to a scan.schedules:write. Reading a schedule needs
schedules:read.
2. Set the schedule
PUT, not POST, because a schedule is a singleton per asset: sending the
same request twice must leave one schedule, and a partial unique index enforces
that even under a race.
Three things about this request are worth understanding rather than copying.
The frequency is a preset. hourly, daily or weekly, and nothing else
— there are no cron expressions. A cron expression brings timezone semantics,
daylight saving and “the 31st of February”, an entire class of bug, in exchange
for letting you pick 03:00. Nobody needs a container rescan at a specific
minute; they need it often enough to notice a newly published critical within a
working day. Intervals are stored in seconds and everything is UTC, which is
precisely what “every 24 hours” needs so that it does not become “every 23 or
25 hours” twice a year.
Container images only, and only tagged ones. An asset first seen through a
digest-pinned reference has no tag to re-resolve. A “rescan” of immutable
content would either re-read identical bytes forever or, if the missing tag
were defaulted, silently scan :latest — a different image from the one you
believe you are watching. The request is refused at creation with a message
saying so, and refused again at run time as defence in depth.
The first run is immediate. It is not one interval out. A user who turns on
daily scanning wants to know the state of the asset today; waiting 24 hours for
the feature to do anything reads as it being broken. The occurrence phase is
then set by that first run, so a schedule enabled at 09:00 runs at 09:00
thereafter.
3. Watch the first run happen
last_run_at set and next_run_at about
a day ahead. A new scan will have appeared on the asset:
4. Read what the schedule says about itself
Three fields on the schedule are health, not configuration, and they are the reason to look at this endpoint again in a week.missed_runs counts occurrences that came due while TRUSTIVAN was not
running. If the platform is down for thirty days, thirty daily runs come due at
once — and TRUSTIVAN runs one, advances in whole intervals past now, and
counts the rest. Replaying all thirty would be thirty identical scans of one
image telling you nothing the last one alone would not have. Silently setting
the next run to “now plus a day” would make a week of downtime
indistinguishable from a week of successful runs. So the gap is collapsed and
then reported, because a gap in coverage nobody is told about is
indistinguishable from continuous coverage. Advancing from the planned time
rather than from now preserves the phase: a schedule that ran at 02:00 keeps
running at 02:00.
consecutive_failures and last_error are what a failing schedule
looks like. On failure, last_run_at is deliberately not advanced — a run
that failed did not happen, and advancing it would let a schedule whose
registry became unreachable report “last run: this morning” forever while
scanning nothing. The next run moves to a backoff rather than a full interval,
so a transient registry outage costs minutes; the backoff never exceeds the
schedule’s own next occurrence, so it cannot quietly turn a daily schedule into
a weekly one. A schedule never gives up: the asset is still yours, and a
security product deciding on its own to stop looking is the wrong failure mode.
Error messages here are sanitised before they are stored, because registry
errors embed URLs and a URL can carry a token.
5. Pause or remove it
Disabling keeps the schedule and its history:204 on success. An asset with no schedule answers 404 on the read — not
200 with a null — because an asset with no schedule and an asset that is not
yours must look identical.
One thing that happens without you
Each run rebuilds the image reference from the asset’s stored coordinates and pushes it back through validation against the current configuration, rather than reusing the string from the last scan. A schedule created while private registries were permitted outlives that setting; if an operator turns private registries off, every stored reference pointing at an internal address must stop being fetched, including the ones already scheduled. Trusting the stored string would leave a permanent, self-renewing exception to a security setting, created before the setting changed and invisible afterwards. The practical consequence: a schedule can start failing because a policy was tightened, not because anything about your image changed.last_error will say
so.
What you have now
An image that is checked every day without anyone asking, whose findings keep their identity and their decisions across every run, and whose schedule tells you when it has been failing or when coverage has a gap. From here:- Webhooks — hear about
scan.completedinstead of polling for it. - Gate a pipeline on a policy — turn what the rescans find into a build decision.
- Troubleshooting scans — when a run keeps failing.

