# Schedule editor

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.

- Registry name: `schedule-editor` (`@candor/schedule-editor`)
- Tier: Pro. Installing it needs a Pro, Premium or Team licence key: the person runs `npx candor-ui key <key>` themselves; never ask for the key in chat or write it to a file.
- Docs: https://candor.themethodic.com/docs/schedule-editor

## Install

```bash
npx candor-ui add schedule-editor
```

Run `npx candor-ui@latest init` once first (https://candor.themethodic.com/get-started.md). Without the CLI: `npx shadcn@latest add @candor/schedule-editor`.
It brings in: button, chat, checkbox, combobox, input, locale, locked, motion, radio-group, textarea, tokens, use-announcer, utils.
npm packages: lucide-react@^1.67.0.

## Example

```tsx
"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>

Props:

- `value?: ScheduleDraft | 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.
- `agents: AgentIdentity[]`: The agents that can run a job. None is chosen for a new job.
- `timeZones?: string[] | undefined`: The time zones offered, as IANA names. Defaults to every zone the browser knows.
- `onSave: (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.
- `busy?: boolean | undefined` (default `false`): The app is saving: Save shows it and can't be pressed again.
- `error?: string | undefined`: Why the last save failed, in the app's words, shown above the buttons.
- `now?: Date | string | number | undefined`: The moment the next runs count from. Left out, the present moment, which moves on as each run passes.
- `weekdays?: number[] | undefined` (default `defaultWeekdays`): The 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.
- `className?: string | undefined`

## Functions

### nextRuns

```ts
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 (`scheduleEditor`)

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

- `createHeading`: New scheduled job
- `editHeading`: Edit scheduled job
- `name`: Name
- `namePlaceholder`: Morning health check
- `repeats`: Repeats
- `hour`: Every hour
- `day`: Every day
- `weekday`: Every weekday
- `week`: On chosen days
- `month`: Once a month
- `at`: At
- `minutePast`: Minutes past the hour
- `days`: Days
- `dayOfMonth`: Day of the month
- `dayOfMonthHint`: 1 to 28, so every month has the day.
- `timeZone`: Time zone
- `timeZoneSearch`: Search time zones…
- `agent`: Agent
- `agentPlaceholder`: Choose an agent
- `prompt`: What the agent does
- `promptHint`: The agent gets this as its instruction on every run.
- `inWords`: In words
- `ruleHour`: { zero: "Every hour, on the hour", one: "Every hour, {count} minute past", other: "Every hour, {count} minutes past" }
- `ruleDay`: Every day at {time}
- `ruleWeekday`: Every weekday ({days}) at {time}
- `dayRange`: {first} to {last}
- `ruleWeek`: Every {days} at {time}
- `ruleWeekNone`: No days chosen, so it never runs
- `ruleWeekdayNone`: No working days are set, so it never runs
- `ruleMonth`: On day {day} of every month at {time}
- `ruleZone`: {rule}, {zone} time
- `nextRuns`: Next five runs
- `nextRunsSource`: Worked out here from the rule above, in {zone}. Your scheduler has the final say.
- `noRuns`: No runs in the next year.
- `unknownZone`: This browser doesn’t know the time zone {zone}, so the next runs can’t be worked out here. Check the name with your scheduler.
- `clockChange`: The clocks change that day, so this time doesn’t exist in {zone}. Check what your scheduler does.
- `clockRepeat`: The clocks go back that day, so this time happens twice in {zone}. Check what your scheduler does.
- `runAt`: {date}, {time}
- `announceNext`: Next run {when}.
- `needsName`: Name the job first.
- `needsDays`: Choose at least one day.
- `needsAgent`: Choose the agent that runs it.
- `needsPrompt`: Say what the agent does.
- `needsZone`: Choose a time zone.
- `badTime`: Enter a time.
- `badMinute`: Enter a minute from 0 to 59.
- `badDay`: Enter a day from 1 to 28.
- `save`: Save schedule
- `create`: Create schedule
- `cancel`: Cancel
- `saving`: Saving…
- `saveFailed`: Saving the schedule failed.

## Types

### ScheduleRecurrence

How often a job runs. Times are wall-clock times in the schedule's own time zone.

```ts
/** 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.

```ts
{
  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.

```ts
{
  /**
   * 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

Import: `import type { AgentIdentity } from "@/lib/candor/chat"` (item `chat`, https://candor.themethodic.com/docs/chat.md)

```ts
{
  id: string;
  /** The displayed name, e.g. "Architect agent". Never derived from model output. */
  name: string;
  role: AgentRole;
  specialty?: string | undefined;
}
```

### AgentRole

Import: `import type { AgentRole } from "@/lib/candor/chat"` (item `chat`, https://candor.themethodic.com/docs/chat.md)

What an agent does: "assistant", "coder", "reviewer"… The app registers each role's glyph and hue (AgentRolesProvider).

```ts
string
```

## Motion

- **run** (arrive, rise): A run that joins the next five because the person changed the rule rises in; never as time passes.
- **run-out** (pop, scale): A 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.

## Rules for using Candor UI

- Components take plain typed data. Never pass model output as HTML or render it as a link; messages are typed blocks.
- A control that can't be used says why: wrap it in `<Locked reason="…">`, don't just disable it.
- Don't show a value you don't have: leave it out (it renders as —) rather than guess. An estimate is labelled as one.
- Nothing is pre-selected in a decision a person makes, and the agent is named from your app's data, never from model output.
- Your app's words go in `LocaleProvider`: its fixed strings in `strings`, its nouns in `nouns`; entity kinds and agent roles in `EntityKindsProvider` and `AgentRolesProvider`.
