Building apps

Scheduled work (Alpha)

Cron routes, the caller-less trigger model, what a cron cannot do, and where scheduled work on a personal account belongs instead.

Alpha. Expect this to change. Scheduled routes are the newest part of the platform. The caller-less trigger model below is settled and the refusals are deliberate, but the limits they produce are under review. Do not build an app whose core loop needs a cron to do something this page says it cannot.

Declare a schedule in the manifest:

crons:
  - schedule: "0 6 * * *"
    path: /api/refresh

A cron invocation has no caller

ctx.user is null and ctx.trigger is "cron". That one fact produces every limit on this page.

What a cron cannot do

Refused (409)Why
agents.start()A run is owned by (app, caller). No caller, no owner
agents.get()The same ownership pair. A cron cannot even poll a run that an http invocation started

Both refuse up front, before any upstream call.

The limit that matters most is your own authorization code. A route that reads ctx.user.roles or filters by ctx.user.id will throw under cron. Or worse, it will return everything, because the flat store enforces nothing. Guard early:

if (!ctx.user) return c.json({ error: "cron cannot do this" }, 409);

What a cron can do

Everything else: db (including db.scoped(), where the owner is an argument rather than the caller), files, SQL, query / savedQueries, llm, email, appUsers(), secrets, egress, and org and service connectors.

That is wider than it first looks. A backend function's authority is its ratified run_as: app manifest, so ctx.user is attribution rather than permission. Cron loses only the surfaces whose authority belongs to one specific person.

Connectors work under cron since CLI 0.3.0. A connector's credential belongs to the row rather than the caller, so a scheduled route can call one.

Operational rules

  • The route must accept POST. The scheduler sends POST. A GET-only route returns 404 on every fire and looks like a broken schedule rather than a bug in your code.

    app.post("/api/refresh", async (c) => { /* ... */ });
  • At least once, and runs may overlap. Use ctx.invocationId as an idempotency key and make external side effects safe to repeat. Never promise "exactly once".

  • Caps: 5 schedules per app, 1-minute minimum.

  • A schedule pauses with a visible reason if the current deploy has no backend function.

  • Under railcode dev cron is not scheduled. Trigger the route by hand, and remember that it is a POST.

Scheduled work on someone's personal account

Do not wait for the app cron. The platform already does this on the agent side, with a safer identity:

  1. Create a personal agent and give it the connector.
  2. Give the agent its own schedule: railcode agent schedule set <agent> --cron "0 9 * * *".
  3. The agent writes its results into its owner's user scope.
  4. Your backend function reads them with db.scoped(ownerUuid).

The agent's identity is fixed at creation, and its manifest was ratified against that identity. If the owner leaves the org, the run fails loudly instead of acting as somebody else. That is why this does not live on the app cron.

agents/proposals in the examples repo is the worked version of this.

On this page