InlayDocs

Schedules

Cron-triggered runs that act as an end user — plus run-as, the on-demand sibling.

A schedule fires an agent on a cron cadence, server-side, with no browser involved. Each fire starts a fresh conversation that acts as a named end user — their durable memory, usage metering, and retrieval scope — and lands in that user's conversation history like any run.

This is a management-tier surface: schedule management rides an operator api-key or the dashboard, never a customer session. (The dashboard's Scheduled-tasks page does all of this with a live cron preview; this page is the API/SDK half.)

// Schedule management and run-as are MANAGEMENT-tier: an operator api-key rides
// as the bearer token, server-side only — never a customer session. The API
// origin defaults to production.
const ops = createOps({ session: process.env.INLAY_SECRET_KEY! });

Creating one

async function createDailyBrief(ops: OpsFacade, agentId: string, userId: string) {
  return ops.createSchedule(agentId, {
    cron: "0 9 * * *", // standard 5-field cron — 9am daily
    timezone: "America/Toronto", // IANA; default "UTC"
    task: "Run the morning brief.", // the run's initial user message
    userId, // the end user the runs act AS
    // version: 3, // optional pin; omit to track the latest published version
  });
}
  • cron is standard 5-field cron evaluated in timezone (IANA; default UTC). A bad expression or zone is a 400 at create time, and the first nextRunAt is computed immediately — a schedule that would never fire cannot be created.
  • task is the kickoff instruction. It becomes the run's first user message, so the agent runs on it.
  • userId is who the run acts as (below). Forgery-resistant values are enforced: reserved ids like shared / anonymous / system:-prefixed are rejected.
  • version pins the schedule to an immutable agent version. On update it is a tri-state: omit it to leave the pin alone, pass null to clear it (track latest-published), pass a number to pin. A pin is validated against the agent's version list, so a typo fails at write time — not as five hundred failed runs.
  • setScheduleEnabled pauses/resumes; updateSchedule edits cron, timezone, task, enabled, and the pin (a cadence change recomputes nextRunAt); deleteSchedule removes.

The act-as model

A scheduled run executes under a system principal acting as the end user — not as the operator, and not as a faceless batch job. Concretely: the run's usage meters against that user (their quota gate applies), their durable user-scope memory is what the agent sees, and their retrieval ACL applies. The conversation appears in their history — useInlayHistory / GET /v1/conversations/mine — with task as the first user message.

The corollary: schedule non-interactive agents. A run that pauses for input or a client tool has nobody to answer it, and a paused run counts as a failure for the circuit breaker below.

The circuit breaker

Schedules are self-protecting:

  • A fire whose conversation does not reach completed is a failure — failed, cancelled, and paused all count.
  • After a failure the schedule backs off 10 minutes and increments consecutiveFailures; past 5 consecutive failures it auto-disables.
  • A success resets the counter and advances to the next cron occurrence — missed beats are skipped, not replayed.
  • A quota denial (the act-as user is out of allowance) backs off without feeding the breaker — a billing condition, not a broken agent.

Listing

Two lists, two tiers, and the difference matters:

  • listTenantSchedules — admin-only, every agent and every act-as user in the tenant, including each schedule's task and userId. Never render this to end users.
  • listMySchedules — the end-user-safe view (a customer session): only the caller's own schedules, and only the whitelist fields (MySchedule drops task and userId). Safe to render as a "your scheduled tasks" page.
async function mySchedules(ops: OpsFacade) {
  // The end-user-safe list (a customer session): only this user's schedules,
  // only the whitelist fields — no `task`, no `userId`.
  const schedules = await ops.listMySchedules();
  return schedules.map((s) => ({ id: s.id, nextRunAt: s.nextRunAt, enabled: s.enabled }));
}

Run-as: the on-demand sibling

runAs fires one synchronous run as an end user, right now — for an operator "run it now" button, a support replay, or wiring your own trigger (a webhook, a queue) where the cadence is yours.

async function fireNow(ops: OpsFacade, agentId: string, userId: string) {
  // The on-demand sibling of a schedule: one sync run, acting as the end user.
  const { conversationId, status } = await ops.runAs(agentId, {
    userId,
    task: "Summarize this week's numbers.",
  });
  return { conversationId, status };
}

createSchedule and runAs deliberately do not auto-retry on network failure: neither is idempotent, and a silent retry would double-create or double-fire.

Honest limits

  • At-least-once. Fires are claimed under a 10-minute lease; a crashed worker's claim is re-taken, and a run longer than the lease can fire twice. If that matters for your agent, keep scheduled runs short or make their effects idempotent.
  • No provenance marker yet. A scheduled run is indistinguishable from the user's own action in the audit trail today — a triggered_by field is tracked follow-up work.
  • Schedules run on the persistent (Postgres) deployment, not the in-memory dev mode.

Next

Versions and publishing — what a schedule's version pin interacts with: drafts, immutable versions, and rollback.

On this page