schedule.create
Schema
Input = { when: string, // ISO 8601, or a phrase like "tomorrow 9am" / "in 2 hours" title: string, // max 160 chars action?: Record<string, unknown>, // what to do when it fires, e.g. { skill: 'signal.raise', input: {...} }}
Output = { scheduleId: string | null, when: string, // resolved ISO instant when parseable, else the original phrase scheduled: boolean, note?: string,}Parsing “when”
parseWhen() (packages/skills/src/desktop/index.ts) handles three shapes without any model call:
a direct ISO/parseable date string, "in N minute(s)/hour(s)/day(s)/week(s)", and
"today"/"tomorrow" optionally followed by a time ("tomorrow 9am", "today 14:30"). Anything
it can’t parse is passed through as-is to the scheduler adapter, which may interpret it further, or
simply store the phrase.
What actually fires it
Like signal.raise, this goes through an injectable ctx.schedule adapter, and the running API
wires a real one (scheduleAdapterFor(), apps/api/src/modules/studio/skills.ts): create() stores
the reminder as an open reminder Signal with the resolved
when as its dueAt. computeNow() withholds a signal until its dueAt has passed, and the Twin’s
periodic tick (fireDueSchedules(), apps/api/src/twin/scheduler.ts) ripples it onto the desktop
the first tick after it comes due, stamping it firedAt so it fires exactly once — nothing sits
unfired, and nothing repeats. If action names a skill ({ skill, input }), the fired signal carries
kind: 'schedule.fire' and an “Do it” / “Not now” pair: answering “Do it” runs that skill through the
same approval path as any other world-acting
request, exactly as if a human had just asked for it.
Example
curl -s -X POST http://localhost:4000/api/studio/skills/invoke \ -H 'content-type: application/json' -b cookies.txt \ -d '{"name":"schedule.create","input":{"when":"tomorrow 9am","title":"Follow up with Jordan"}}'{ "ok": true, "output": { "scheduleId": "sig_7fa2...", "when": "2026-09-07T09:00:00.000Z", "scheduled": true } }