Module Deep Dive

Schedulers & Invoke API Triggers

The two ways an agent runs with nobody watching — on a clock, and on an HTTP call — and the machinery that keeps unattended work safe, cheap and accountable.

Overview

Everything on this page exists because of one difference: nobody is sitting there when the run happens.

Most of Derbee Studio is about a person asking an agent something and reading the answer. These two modules are about the other case. A schedule fires an agent or a multi-agent team on a recurring cadence or at a single future instant. An invoke trigger lets an external system call one over HTTP with an API key. Different entry points, same destination: both end up inside the ordinary agent and team executors, and both answer to the same quotas.

What changes is the safety model. In chat, a wrong answer is read and corrected in seconds. A schedule that goes wrong at 03:00 spends the tenant's model budget on a timer, every hour, until somebody notices. So the protections here are structural rather than supervisory — a lock that makes duplicate runs impossible, a lease that survives a crash, a failure streak that pauses the schedule before it burns more tokens, and a quota gate that treats a timed run exactly like a paid API call.

Two ways in, one execution path

The trigger differs. Everything after the gate is identical to a normal run.

internal clock external caller Schedule fires API key invoke Quota + identity gate Agent or Team executor Recorded + metered
Two triggers, one execution path — the executors do not know how they were called

What unattended execution costs you

Four problems that do not exist in chat, and the answer to each.

Two servers, one job
Run more than one instance of the app and both will see the same schedule come due. Solved by making the schedule document its own lock: claiming it is a single atomic write, so exactly one instance wins.
A crash mid-run
A process that dies holding a lock would stop that schedule forever. Solved by a lease — the lock carries an expiry sized to the longest the run could legitimately take.
Coming back from downtime
A schedule that missed six hours of slots would otherwise fire six hours of real runs at tick speed. Solved by the misfire policy, which either drops the backlog or allows exactly one catch-up.
A schedule that is simply broken
A prompt that always fails would keep spending tokens on a timer. Solved by auto-pause: five consecutive failures and the schedule stops itself and reports why.
None of this is best-effort. The recurrence engine is pure — it takes its reference instant as an argument and touches no clock, no database and no framework — so every scheduling rule on this page is asserted by unit tests that need neither a database nor the passage of real time. There are 45 of them, covering the cadence maths, the DST edge cases, the misfire rules, the lease formula and the permission model.

Modules

The Schedule

One record type fires both an Agent and a Multi-Agent Team. A single field says which, and it is the only thing that differs between the two — so one model, one dispatcher and one screen serve both, and neither the agent nor the team document is touched by being scheduled.

What a schedule holds
Target
An agent or a team, referenced by id. Deleting the target does not leave the schedule firing into nothing — the delete handlers disable every schedule pointing at it, with a reason the interface can show.
Trigger
recurring with a recurrence, or once with a single instant.
Payload
The query to send, an optional variables bag, and how the conversation is scoped — a fresh session per run, or one fixed session every run appends to. Optionally an email, which is the identity the run is attributed to.
Bounds
An active window with a start and an end, and a maximum number of runs (zero meaning unlimited). Both are checked independently of the cadence.
Execution policy
Misfire policy, retry attempts (1–5) with a backoff in seconds (0–900), and a hard timeout — ten minutes by default, floor ten seconds, ceiling one hour.
Live state
The next scheduled instant, the last outcome and its duration, a pointer to the last run record, a consecutive-failure counter, and the three lock fields.
Statuses

Five, and they are not interchangeable — the difference between them is who stopped it and why, which is exactly what someone looking at a silent schedule needs to know.

  • active due to fire
  • paused stopped by a person
  • completed ran out of occurrences
  • error auto-paused after failures
  • disabled its target was deleted
The document is its own lock. There is no queue, no external scheduler and no second system to keep in sync. The dispatcher claims a due schedule with one atomic update on its lock fields — which is what makes running several app instances safe, and what gives overlapping runs skip semantics for nothing: a schedule that is still executing is still locked, so it cannot be claimed again.
Indexes

One index carries the whole design: status together with the next-run instant. It is what the dispatcher's claim query hits, once per tenant per tick, and an idle tenant therefore costs one indexed miss and nothing else. The rest are listing support.

The Recurrence Engine

All the cadence maths lives in one pure module. No database, no framework, and — importantly — no implicit clock: every entry point takes the reference instant as an argument. That one decision is what makes the whole of scheduling testable without waiting for real time to pass, and it is why the interface can show you a live "next five runs" preview computed by exactly the code the dispatcher runs, rather than a second implementation that might disagree with it.

Units and their bands

Five units, each with its own allowed interval range. The five-minute floor on minute cadences is a product rule, and it is enforced in two places — the API and the schema — on purpose, because the API is exposed to tenants and a limit that lives only in the interface is not a limit.

UnitIntervalExtra controls
minutes5 – 60Rolling — no anchor
hours1 – 24Optional minute of the hour
days1 – 365Time of day
weeks1 – 52Chosen weekdays + time of day
months1 – 12Day of month, or the last day; clamp or skip
Two kinds of cadence

Sub-hourly cadences are rolling: the next instant is derived from the slot that just ran, never from the moment the run finished, so a slow run cannot make an every-fifteen-minutes schedule drift later and later. Calendar cadences instead hold wall-clock intent — "09:00 daily" is stored as an intent plus a zone, and the actual instant is worked out at compute time.

For intervals greater than one, the phase is anchored to the schedule's creation instant. "Every three days" means every third day counted from when you created it, not every third day of an arbitrary epoch — so the same recurrence created on different days produces different, predictable days.

Timezones, DST and the gap

Zones are resolved with the platform's own internationalisation data rather than a date library. A schedule stores wall-clock intent and an IANA zone; the instant is derived at compute time, so a daylight-saving shift is absorbed automatically and "09:00" stays 09:00 on both sides of it.

The spring-forward gap gets explicit handling, and the bug it prevents is subtle. Converting a wall-clock time to an instant takes two passes, because the first offset reading is taken at the wrong instant near a boundary. But on the day a zone jumps straight from 02:00 to 03:00, a schedule set for 02:30 asks for a time that does not exist — and the two-pass result lands just before the gap, which would run the job an hour early. So the result is read back and checked: if the wall clock doesn't say what was asked for, the answer is resolved forward past the transition instead. Never backward.
The short-month problem

"The 31st" has no honest answer in February, so the schedule says which answer it wants: clamp moves it to the last day of the month — the default, and the way most people read it — or skip passes over short months entirely. A dedicated "last day of the month" setting tracks month length on its own and needs neither.

Bounded by construction

Both calendar walks are probe-limited — four hundred days, twenty-six months. A recurrence that can never be satisfied returns no occurrence rather than becoming an unbounded loop inside the dispatcher's tick.

Cron is rendered, never parsed. A cron string is produced for display and export, and only when it would be faithful — a rolling forty-five-minute cadence, "the last day of the month", and any multi-week interval all return nothing rather than a cron expression that means something subtly different. The dispatcher itself never reads cron at all.

The Dispatcher

A background loop that wakes every thirty seconds, walks the tenants, and claims whatever is due. It starts after the HTTP server is already listening, so a slow tenant handshake can never delay the application becoming ready, and it can be switched off entirely on instances that should only serve traffic.

Deliberately cheap when idle

Nothing is read, computed or fetched until a schedule is actually due. The claim is a single indexed query, the tenant list is cached for a minute, and an idle tenant costs one index miss per tick and zero tokens. This matters because the loop runs forever, in every deployment, whether or not anybody uses scheduling.

The claim

One atomic update both claims the schedule and stamps its lease. It has to be one write: reading the timeout first and extending the lease afterwards would leave a window in which a long run is protected by a short lease. The lease is therefore computed inside the database, from the schedule's own fields, against the database's clock — which also removes any assumption that two competing instances agree about what time it is.

Fairness and back-pressure
Claims per tenant
Capped per tick (twenty by default) so one busy tenant cannot starve the others inside a single pass.
Runs in flight
Capped per tenant at the executors' own concurrency ceiling — eight. Once that is reached the dispatcher stops claiming and leaves the rest in the database.
Overlapping ticks
Impossible. A pass that runs long is not allowed to stack up behind itself.
An unreachable tenant
Logged and skipped. One tenant whose database is down never stalls the others, and the connection layer already fast-fails after a recent failure rather than waiting out a timeout every tick.
Why the in-flight cap is a claiming rule, not a queueing rule. Claiming work this instance cannot start is strictly worse than leaving it in the database. A queued run holds its lock and burns lease time before doing anything at all — while another instance, which could have run it immediately, is blocked from claiming it. So the ceiling is enforced before the claim rather than after it.
Two checks before anything runs

A claimed schedule is still not certain to execute. It is dropped if the current instant falls outside its active window, and dropped if the slot is stale enough to have been missed while the service was unavailable. Either way the schedule is realigned and released, and a skipped run is written to the history — because a gap that is visible is a support question, and a gap that is silent is a bug report.

There is a small but telling detail in the realignment: when a schedule is claimed before its window has opened, the next instant is computed from the window's start rather than from now. Computing from now would re-claim and re-skip the same job on every single tick until the window finally opened.

Crash recovery

Two independent mechanisms, because a crash leaves two different messes. The schedules come back on their own when their lock leases expire — nothing has to notice. The run records left marked as still-running are closed by a sweep that runs once per database connection per process, so the history does not accumulate rows that will never finish.

On an orderly shutdown the loop stops claiming, waits briefly for in-flight runs to finish and release their own locks, and hands whatever is left to the lease.

Tenancy

Schedules live in each tenant's own database. A standalone deployment keeps them on the master connection instead — but only when there are genuinely no tenants, because polling master in a multi-tenant install would create the collection and its indexes on a database that will never hold a schedule, and then poll it forever.

The Lock Lease

A claimed schedule holds its lock for as long as the run could possibly take — not for one attempt. Under-sizing the lease is precisely what lets a second instance claim a schedule that is still executing, so the budget has to cover the whole retry envelope: every attempt's timeout, plus every backoff between them, plus headroom.

The backoff sum is not linear. Backoff grows with the attempt number, so three attempts wait one backoff then two — the total across n attempts is the triangular sum, not n times the backoff. Getting that wrong under-sizes the lease exactly on the schedules that need it most: the ones that keep failing.
The formula exists twice, on purpose

Once in application code, for the heartbeat that renews the lease while a run is in flight. Once as a database expression, so the dispatcher can derive the lease from the schedule's own fields inside the atomic claim. They have to stay identical — so they live side by side in one small file, and a test asserts that the two agree across a matrix of specifications rather than trusting that a future edit will touch both.

The heartbeat

While a run is executing, the lease is pushed out periodically — at a third of its own length. Without this, a run that retries, or that waits for a slot in the executor's concurrency limiter, can outlive its lease and be claimed a second time.

The renewal is guarded on ownership. If this runner's lease genuinely did lapse and another instance legitimately took the schedule, the renewal matches nothing and the lock is not stolen back. A heartbeat that renewed unconditionally would be worse than no heartbeat at all.

The Runner

Executes one claimed schedule, and owns it from that moment on. Its contract with the dispatcher is absolute: whatever happens, it must advance the cadence and clear the lock. A leaked lock silently stops a customer's schedule until the lease expires, so the release lives in a block that cannot throw — and if even the final write fails, there is a last-resort attempt that does nothing but take the lock off.

Order of work
  1. Resolve the target. Gone? Disable the schedule rather than fail every slot forever.
  2. Quota gate. Over budget is skipped, not failed — nothing is broken.
  3. Open a run record marked running, and start the lease heartbeat.
  4. Invoke under a hard timeout, retrying per the schedule's policy.
  5. Close the run record with status, duration, usage, trace id and a response preview.
  6. Advance the cadence and release the lock in one write.
  7. Report telemetry — best effort, never allowed to affect the outcome.
Sessions and substitutions

A schedule either opens a fresh conversation per run — the default, and the right choice for a report — or appends every run to one fixed session, which is what you want when the agent should remember what it said last time.

The query supports a small set of substitutions, resolved against the slot rather than the wall clock, so a run that started late still asks about the moment it was scheduled for:

  • now the slot, in full
  • date the slot's calendar date
  • lastRunAt the previous run
  • runCount how many so far

Substitution is skipped entirely when the query contains no placeholder at all — the common case gets no scan and no allocation.

Timeouts, retries and misfires
Hard timeout
Enforced with the executors' own cancellation support, so an abandoned run actually stops rather than continuing in the background.
Never retried
A timeout, or a cancellation from elsewhere. Retrying either would simply abort again, on the tenant's budget.
Cadence after a run
Derived from the slot that ran, not from the finish time — so a slow run never makes the schedule drift. But it is never handed back a slot in the past; if the computed next instant has already gone, it realigns to the present.
Misfire: skip
The default. A slot more than one full cycle overdue was missed while the service was down, so it is dropped — answering a question about a moment that has passed is rarely what was wanted.
Misfire: fire-once
Never drops a slot. Its single catch-up run is the overdue slot itself, after which the cadence realigns to the present.
One-time schedules
Never stale. A missed appointment is still wanted.
A run that timed out does not blame an absent user. Both executors report cancellation with the same message they use for the chat stop button — true there, wrong here, since nobody pressed anything. The runner tracks whose timer fired and rewrites the failure accordingly, so the run record says the run exceeded its timeout rather than claiming a user stopped it. A small thing that decides whether the history is trustworthy.
Auto-pause

Five consecutive failures and the schedule stops itself, with status error and the last failure recorded. A broken schedule must not keep spending the tenant's model budget on a timer. Resuming clears the counter and realigns from now — a long pause does not produce a burst of catch-up runs the moment it is lifted.

Skipped slots are excluded from all of this. A skipped slot never reached a model, so it does not count toward the run total, does not move the failure streak, and cannot trip the auto-pause.

Teams are validated first

A multi-agent team is checked for graph validity before it is invoked. An unrunnable team fails with a diagnostic summary rather than an opaque executor error — which, on an unattended run nobody is watching, is the difference between a fixable report and a mystery.

Run History

Every execution writes an authoritative record: which slot it belonged to, when it actually started, how long it took, its outcome, the session and message it produced, a trace id that deep-links into the existing log viewer, token usage, and a truncated preview of the answer. The full answer lives in the session and the trace log — the preview exists so the runs list can show something useful without loading it.

This collection is deliberately separate from analytics, and the reason is a real failure mode. Analytics ingestion is fire-and-forget, and a single invalid sub-document silently drops the entire record. A scheduler reporting its own history from there would under-report — runs that definitely happened would be missing, with no error anywhere. So run history is written synchronously to its own collection, and analytics still receives every run for the cost and token dashboards. Two destinations, two different jobs.
Reading the records
Slot vs start
Both are stored. The distance between them is the dispatcher's drift — how late the tick picked the job up — and it is measurable per run rather than inferred.
Trigger
Whether the dispatcher claimed it or somebody pressed Run now. A manual run is recorded exactly like a scheduled one, but disturbs neither the cadence nor the lock accounting.
Outcomes
running success failed skipped timeout — five, because "it didn't run" and "it ran and broke" are different answers.
Retention
Expires on the same configured clock as analytics, and reconciled rather than declared — a TTL that changes would otherwise make the index creation fail against the existing one.
Run now

The test my schedule button fires immediately without disturbing the cadence. It still claims the lock exactly as the dispatcher does — so a manual run can collide neither with a scheduled one nor with a second impatient click. Each of those would be a real, billable duplicate run. The request answers straight away with an accepted status and the client polls the run list, because a run can take minutes.

Editing a schedule while a run is in progress is refused for the same reason: mutating a job the dispatcher is already executing is not worth the race it creates, and the run finishes in seconds.

Quotas & Permissions

A scheduled run spends the tenant's budget exactly like an API call does, so it answers to exactly the same limits. Two gates run before any model is touched: the target's own daily cap, and — when the schedule names an invoking identity — that person's per-minute invoke budget.

The daily cap is counted from two sources, and that is not redundancy. Analytics cannot be the only source, for the reason above: it drops records silently and would quietly let the cap be exceeded. So scheduled runs are counted from the synchronously-written run history, and analytics supplies only what it alone knows about — chat and API invocations. Records written before scheduling existed carry no trigger field at all, and the query is written so those still count correctly.

Both gates fail open, consistent with the rate limiter itself: a counter outage must not silently stop every schedule in the tenant. And being over quota produces a skipped run, never a failure — so a busy day cannot push a perfectly healthy schedule toward auto-pause.

Who may create one

Scheduling brings its own permission check rather than reusing the application's general authorisation middleware, and the reason is stated plainly in the code: that middleware currently short-circuits before any check — a documented, project-wide temporary bypass — so applying it here would enforce nothing. Changing it would alter behaviour on every route in the application, which is not this feature's call to make. It matters more here than on an ordinary screen: a schedule spends the tenant's model budget on a timer, with nobody watching.

Compatibility
The schedules resource did not exist before this feature, so no existing role carries it. Requiring it outright would make scheduling unreachable for everyone until an administrator edited every role. It falls back to the agent and team permissions instead — you may schedule what you may already manage — with no migration.
Explicit wins, both ways
A role that names the schedules resource is authoritative. A narrow action list is a deliberate limit and is never widened by the fallback; an explicit empty permission denies rather than falling through.
Fails closed
An unresolvable role is an error, not an open door. The rule itself is a pure function, so who may spend the budget on a timer is asserted by tests with no request and no database.
Blast radius
A ceiling on live schedules per tenant — a limit on how much unattended work one tenant can have pending, not a licence gate.
Every detail route funnels through one loader. A bare lookup by id would let one tenant read another's schedule by guessing. The ownership filter matters just as much: without it, any authenticated user in a tenant could read another user's prompt, retarget their schedule, or trigger a billable run — just by knowing its id.

Validation is strict about one field in particular. The invoking email is the rate-limit subject, so an arbitrary string would hand out a private quota bucket, and a typo would silently detach the schedule from the limits of the person it is meant to run as.

Invoke API Triggers

The external trigger. Two endpoints — one for an agent, one for a team — each authenticated by an API key that is bound to exactly one target. Calling a team key on the agent endpoint gets an explicit message pointing at the right one, rather than a generic authentication failure.

Routing happens before authentication

The key itself says which database to talk to. A client key carries the client's identifier in the middle of it, so the tenant connection is resolved from the key before anything is validated against it; a system key goes to the master connection. This is what lets a single endpoint serve every tenant without the caller supplying routing information, and without a shared lookup table in front of it.

Key handling
Stored
As a hash, excluded from queries by default — plus an encrypted copy so the key can be shown again in the interface, also excluded by default. The plaintext is returned exactly once, at creation.
Prefix
A short display prefix identifies the key in listings without revealing it — and the prefix in the URL must match the key presented, so a mismatched pair is rejected before any lookup.
Expiry
Set at creation, clamped between one day and a year. A key found to be past its expiry is deactivated on the spot, not merely rejected — the next call does not have to re-discover it.
Last used
Stamped on every successful validation, so a key nobody is calling is visible as such.
Rotation
Regenerating revokes the old key and issues a new one in a single action; revoking is reversible-by-record, deleting is not.
Identity, and why email is required

A key identifies an integration; it does not identify a person. Since rate limits are per-person, the caller must supply and the endpoint must validate an email — so budgets follow the human rather than the key they happen to be calling with. The same identity is then tracked as an API session, which is upserted rather than duplicated, and whose failure is non-fatal: losing the session record must not lose the run.

Two limits, checked differently

The per-minute invoke limit is consumed up front — and a rejected request has its increment rolled back, so a call that was refused never eats budget it was not served for. The daily model-call budget is only peeked up front, because the real number of model calls is not known until the run is over.

Counting model calls is the interesting part. A single run fans out across the executor, guardrails, classifiers and tool loops, so the calls are counted where they actually happen rather than threaded back through every call site — a request-scoped counter, opened at the top of the handler and read once the run finishes. Crucially, the failure path books it too: a run that broke halfway still burned whatever it burned, and the counter is reset as it is read so success and error paths can never double-count the same run.
Cache, cancellation and telemetry

An invoke consults the same semantic cache as chat, scoped to the session and to the fingerprints of whatever assets the query references — but at a stricter similarity threshold than chat uses, which is the right trade for a machine-to-machine caller that cannot glance at an answer and notice it is about the wrong thing. A cache hit is still recorded as a full telemetry event, flagged as a hit and carrying the score, so cache effectiveness is measurable rather than invisible. A lookup failure is logged and skipped — never fatal.

If the HTTP connection drops, the run is cancelled rather than left to complete for a caller that has gone. Every invocation — cached, successful or failed — is attributed to the invoking identity, tagged with its source and its API key, so automated traffic can be told apart from people in the dashboards.

How It Works — Flow Examples

Five diagrams: the tick, the lease, one scheduled run, one API invoke, and what downtime does.

Entry Work Decision Gate / side effect Result

Example 1 — One dispatcher tick Dispatch

The concurrency ceiling is checked before the claim, not after. Two more checks stand between a claimed schedule and a run that actually costs money.

executor ceiling one write wins missed while down Tick — every 30s For each active tenant In-flight below the cap? Leave it in the DB Atomic claim + lease Nothing due Inside the window? Record skipped More than a cycle late? Realign + release Hand to the runner
Cheap checks first — an idle tenant costs one indexed miss and stops there

Example 2 — Sizing the lock lease Lock

The lease covers the worst case a claimed run can occupy: every attempt, every backoff between them, plus headroom for jitter and a slow final write.

Lease for a schedule set to three attempts attempt 1 wait attempt 2 wait ×2 attempt 3 grace heartbeat pushes the expiry out, guarded on the owner a lapsed lease is never stolen back from whoever legitimately took it If the lease were sized for one attempt only expires here a second instance claims a schedule that is still running
Backoff grows with the attempt number, so the total is triangular — not attempts × backoff

Example 3 — One scheduled run Run

From claimed document to released lock. Two of the exits never reach a model at all, and neither counts as a failure.

deleted underneath not a failure timeout is never retried backoff grows per attempt Claimed schedule Resolve the target Disable schedule Quota gate Record skipped Open the run record Invoke under a timeout Retry after backoff Close the run record Advance + release lock
The last step happens whatever went wrong above it — a leaked lock stops the schedule until its lease expires

Example 4 — An API invoke Trigger

The key routes the request before it authenticates it. The daily model budget is peeked on the way in and booked with the real number on the way out — including when the run fails.

before any auth limits follow the person stricter than chat cancelled if the caller drops Invoke with an API key Route by key format Master connection Validate the key hash 401 — rejected Identify the caller 400 — need email Check the rate limits 429 + Retry-After Semantic cache Cached reply Run the agent or team Book usage + telemetry
Every exit is metered — a cache hit and a failure are both attributed to the invoking identity

Example 5 — Returning from downtime Misfire

An hourly schedule, with the service unavailable across two of its slots. The misfire policy is the whole difference between a quiet recovery and a burst of billable catch-up runs.

service unavailable 09:00 10:00 11:00 12:00 13:00 skip — the default resumes at the next real slot fire-once one catch-up a dropped slot is still written to the history as skipped, never left silent
Neither policy replays the backlog — the difference is whether one overdue slot is honoured

Reference Tables

Defaults, limits and endpoints.

Schedule defaults and limits

SettingDefaultRange
TimezoneUTCAny IANA zone, validated
Session modeNew per runNew, or one fixed session
Misfire policySkipSkip, or fire once
Retry attempts11 – 5
Backoff30 s0 – 900 s, growing per attempt
Run timeout10 min10 s – 1 hour
Max runsUnlimited0 means unlimited
Auto-pause5 failuresConsecutive, skips excluded
Lease headroom2 minAdded to the worst case
Response preview2 000 charsFull answer lives in the log

Dispatcher settings

SettingDefaultPurpose
SCHEDULER_ENABLEDonSet to off on instances that should only serve traffic
SCHEDULER_TICK_MS30 000How often the loop wakes
SCHEDULER_MAX_CLAIMS20Claims per tenant per tick — fairness
SCHEDULER_MAX_PER_TENANT50Live schedules a tenant may hold
In-flight per tenant8Matched to the executors' own ceiling
Tenant list cache60 sThe client list changes rarely
Shutdown drain15 sThen the lease takes over
Orphan sweep cutoff1 hourCloses runs left open by a crash

Endpoints

RouteDoes
Schedules — session authenticated, permission checked
POST /agent-schedulesCreate; computes the first slot and enforces the tenant ceiling
GET /agent-schedulesList, filterable by target and status; omits the variables bag
GET …/summaryBadge data for the agent and team lists in one call
POST …/previewNext occurrences for an unsaved draft — stateless
PUT …/:idUpdate; refuses while a run holds the lock
DELETE …/:idSoft delete — invisible to the dispatcher immediately
POST …/:id/pauseStop firing
POST …/:id/resumeRealign from now; refuses if nothing is left to fire
POST …/:id/run-nowFire immediately without disturbing the cadence
GET …/:id/runsRun history, newest first
GET …/:id/runs/:runIdOne run in full, including the preview
Triggers — API key authenticated
POST /invoke/agent/:prefixRun an agent with an agent-bound key
POST /invoke/team/:prefixRun a multi-agent team with a team-bound key
Key management
POST /agent-api-keysIssue a key — plaintext returned once
POST …/:id/regenerateRevoke and reissue in one step
GET …/agent/:id · …/team/:idKeys for one target
PATCH …/:id/revokeDeactivate without deleting
DELETE …/:idRemove permanently

Query substitutions

TokenResolves to
{{ now }}The scheduled slot, full timestamp
{{ date }}The slot's calendar date
{{ lastRunAt }}When the previous run happened, if any
{{ runCount }}How many runs have completed

Rate limits applied to both surfaces

LimitWindowSubjectBehaviour
Invoke1 minuteThe invoking emailConsumed up front; a rejection is rolled back
Model calls24 hoursThe invoking emailPeeked up front, booked with the real count after
Agent / team cap24 hoursThe targetCounted from run history and analytics together

Every limit fails open. A counter that cannot be read allows the request and logs the failure — a storage blip must never take down the whole invoke surface, or stop every schedule in a tenant at once. A limit of zero means unlimited.