Skip to content

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

Terminal window
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 } }