Skip to content
Tier

Schedule editor

Pro

Create or edit a recurring job: its name, how often it runs written out in words, the time zone it keeps, the next five runs worked out from that, the agent that runs it and the instruction it gets. The runs are computed in the browser and say so; a time the clocks skip is flagged, not moved. Save stays locked, saying why, until the job has a name, an agent, an instruction and (for chosen days) at least one day. Pair it with schedule-list's onEdit.

The next runs read in the schedule's own time zone, not LocaleProvider's or the viewer's, since that's where the rule's times are kept. Before the page hydrates they aren't shown, so server and browser render the same markup. They count from now, or from the present moment, and move on by themselves as each run's time passes. A time zone the browser doesn't know is shown as given, with a note that the runs can't be worked out here, rather than failing.

"Every weekday" means Monday to Friday unless you set weekdays to the days your scheduler runs for every: "weekday" (Sunday to Thursday, say); the rule in words names them.

The next runs depend on the rule (the dependent-values pattern): when the person changes it, a run that joins the list arrives and one that leaves pops out, and once they stop changing it screen readers hear the rule and the next run, once, politely.

Markdown
Version 5.2.0· other versions

This page is for 5.2.0, the current release. An earlier release’s page is the Markdown that release published, so the docs match the version you have installed (candor-lock.json records it).

Not in the docs of 1.3.0, 1.4.0, 1.5.0, 1.5.1, 1.6.0, 1.7.0, 1.8.0, 1.8.1, 1.9.0, 1.9.1, 2.0.0, 2.1.0, 2.2.0, 2.3.0, 2.4.0, 2.5.0, 2.5.1, 2.6.0, 2.6.1 and 2.6.2: the page came later, or had another name then.

Docs pages weren’t kept for 1.0.0, 1.0.1, 1.1.0, 1.1.1, 1.2.0 and 1.2.1: they are kept from 1.3.0 on, and before that only llms.txt was published.

Preview

Loading the preview

Install

With the Candor CLI

First time? Get started sets up the registry and theme. Or with shadcn directly: npx shadcn@latest add @candor/schedule-editor

Brings in: button, chat, checkbox, combobox, heading-level, heading-scope, input, locale, locked, motion, radio-group, textarea, tokens, use-announcer, use-settle, utils · npm: lucide-react@^1.67.0

Usage

TSX · The example above
"use client"

import * as React from "react"
import { ScheduleEditor, type ScheduleDraft } from "@/components/candor/schedule-editor"

const agents = [
  { id: "qa", name: "QA agent", role: "tester", specialty: "Health checks and test runs" },
  { id: "an", name: "Analyst agent", role: "analyst", specialty: "Reports" },
]

export default function Example() {
  const [saved, setSaved] = React.useState<ScheduleDraft>()
  return (
    <div className="flex max-w-xl flex-col gap-3">
      <ScheduleEditor
        agents={agents}
        timeZones={["UTC", "America/New_York", "Europe/London", "Asia/Kolkata", "Australia/Sydney"]}
        value={{ title: "Morning health check", recurrence: { every: "weekday", time: "07:00" }, timeZone: "America/New_York", agentId: "qa", prompt: "Check every integration and report anything that failed overnight." }}
        onSave={async (draft) => {
          await new Promise((r) => setTimeout(r, 400))
          setSaved(draft)
        }}
        onCancel={() => {}}
      />
      {saved && <p className="text-xs text-muted-foreground">Saved {saved.title}.</p>}
    </div>
  )
}

<ScheduleEditor>

PropTypeDefaultDescription
valueScheduleDraft | undefined—The job being edited. Without it the editor creates one: every day at 09:00 in the viewer's time zone, with no agent chosen.
agentsrequiredAgentIdentity[]—The agents that can run a job. None is chosen for a new job.
timeZonesstring[] | undefined—The time zones offered, as IANA names. Defaults to every zone the browser knows.
onSaverequired(draft: ScheduleDraft) => void | Promise<void>—Called with the finished job; the button shows it is saving while a returned promise is pending.
onCancel(() => void) | undefined—Shows Cancel; called when it's pressed.
busyboolean | undefinedfalseThe app is saving: Save shows it and can't be pressed again.
errorstring | undefined—Why the last save failed, in the app's words, shown above the buttons.
nowDate | string | number | undefined—The moment the next runs count from. Left out, the present moment, which moves on as each run passes.
weekdaysnumber[] | undefineddefaultWeekdaysThe days "Every weekday" runs on, as day numbers (0 is Sunday, 6 is Saturday). Defaults to Monday to Friday. Set it to what your scheduler does with `every: "weekday"`; with no days, the rule says it never runs.
classNamestring | undefined—Extra classes for the outer element.
headingLevelHeadingLevel | undefined—The level of the form's heading, so it fits under the page's own headings. Default: one below the nearest `HeadingScope`, else 2 (`<h2>`). Its sub-headings sit one level below it.

Functions

nextRuns(recurrence: ScheduleRecurrence, timeZone: string, from: Date, count = 5, weekdays: number[] = defaultWeekdays)

The next count runs of a recurrence after from, in its time zone, looking at most a year ahead. A daily, weekly or monthly time the clocks skip is kept and flagged (clock: "skipped") rather than moved, and one they repeat is listed once, at its first instant, and flagged (clock: "repeated"), because what a scheduler does with them varies. Every hour lists each instant once: an hour the clocks skip isn't there, and both runs of a repeated hour are, flagged. weekdays are the days every: "weekday" runs on (0 is Sunday), Monday to Friday unless given. A zone the browser doesn't know throws a RangeError, as Intl does.

Strings

The fixed words, in English by default. Change any of them under scheduleEditor in <LocaleProvider strings={{ … }}>; the table is exported as scheduleEditorStrings.

KeyEnglish
createHeadingNew scheduled job
editHeadingEdit scheduled job
nameName
namePlaceholderMorning health check
repeatsRepeats
hourEvery hour
dayEvery day
weekdayEvery weekday
weekOn chosen days
monthOnce a month
atAt
minutePastMinutes past the hour
daysDays
dayOfMonthDay of the month
dayOfMonthHint1 to 28, so every month has the day.
timeZoneTime zone
timeZoneSearchSearch time zones…
agentAgent
agentPlaceholderChoose an agent
promptWhat the agent does
promptHintThe agent gets this as its instruction on every run.
inWordsIn words
ruleHour{ zero: "Every hour, on the hour", one: "Every hour, {count} minute past", other: "Every hour, {count} minutes past" }
ruleDayEvery day at {time}
ruleWeekdayEvery weekday ({days}) at {time}
dayRange{first} to {last}
ruleWeekEvery {days} at {time}
ruleWeekNoneNo days chosen, so it never runs
ruleWeekdayNoneNo working days are set, so it never runs
ruleMonthOn day {day} of every month at {time}
ruleZone{rule}, {zone} time
nextRunsNext five runs
nextRunsSourceWorked out here from the rule above, in {zone}. Your scheduler has the final say.
noRunsNo runs in the next year.
unknownZoneThis browser doesn’t know the time zone {zone}, so the next runs can’t be worked out here. Check the name with your scheduler.
clockChangeThe clocks change that day, so this time doesn’t exist in {zone}. Check what your scheduler does.
clockRepeatThe clocks go back that day, so this time happens twice in {zone}. Check what your scheduler does.
runAt{date}, {time}
announceRule{rule}. Next run {when}.
announceRuleAlone{rule}.
needsNameName the job first.
needsDaysChoose at least one day.
needsAgentChoose the agent that runs it.
needsPromptSay what the agent does.
needsZoneChoose a time zone.
badTimeEnter a time.
badMinuteEnter a minute from 0 to 59.
badDayEnter a day from 1 to 28.
saveSave schedule
createCreate schedule
cancelCancel
savingSaving…
lockedSavingSaving the schedule
saveFailedSaving the schedule failed.

Motion

MomentFamilyWhat it does
runarrive, riseA run that joins the next five because the person changed the rule rises in; never as time passes.
run-outpop, scaleA run that leaves because the person changed the rule fades out; never as time passes.

Set with --cd-motion-schedule-editor-<moment>-* in your brand file; the families are in the design reference.

Types

ScheduleRecurrence
How often a job runs. Times are wall-clock times in the schedule's own time zone.
/** Every hour, `minute` minutes past. */ { every: "hour"; minute: number; } /** Every day at `time` ("07:00", 24-hour). */ | { every: "day"; time: string; } /** Every working day at `time`: Monday to Friday, or the days ScheduleEditor's `weekdays` names. */ | { every: "weekday"; time: string; } /** On the chosen days of the week (0 is Sunday, 6 is Saturday) at `time`. */ | { every: "week"; days: number[]; time: string; } /** On `day` (1 to 28) of every month at `time`. */ | { every: "month"; day: number; time: string; }
ScheduleDraft
A scheduled job as the editor reads and saves it.
{ title: string; recurrence: ScheduleRecurrence; /** An IANA time zone, e.g. "Europe/London". */ timeZone: string; /** The agent that runs the job; `null` while none is chosen. */ agentId: string | null; /** The instruction the agent gets on every run. */ prompt: string; }
ScheduledRun
One computed run: when, and whether a clock change makes its wall-clock time not exist, or happen twice.
{ /** * The instant. For a time the clocks skip, the instant that time would have been on the old clock (02:30 skipped * reads 03:30 on the new one), so read `time` for the wall-clock time the rule names. */ at: Date; /** The clocks skip this wall-clock time that day, so it doesn't exist in the zone. Same as `clock === "skipped"`. */ clockChange: boolean; /** "skipped" when the clocks go forward over this time, "repeated" when they go back and it happens twice. */ clock?: "skipped" | "repeated" | undefined; /** The wall-clock time the rule names, "02:30" (24-hour), in the schedule's zone. */ time?: string | undefined; /** The day it falls on in the schedule's zone, "2027-03-14". */ date?: string | undefined; }

Types from other items

The props above take these. Each is defined by another item; import it from the path shown.

AgentIdentity from @/lib/candor/chat
{ id: string; /** The displayed name, e.g. "Architect agent". Never derived from model output. */ name: string; role: AgentRole; specialty?: string | undefined; }
HeadingLevel from @/lib/candor/heading-level
A heading's level in the page's outline: `1` is `<h1>`, `6` is `<h6>`. Every Candor item that draws a heading takes its `headingLevel` as this type; the level is where the heading sits in the outline, never how large it looks.
1 | 2 | 3 | 4 | 5 | 6
AgentRole from @/lib/candor/chat
What an agent does: "assistant", "coder", "reviewer"… The app registers each role's glyph and hue (AgentRolesProvider).
string