# 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`.

Before the page hydrates the next runs aren't shown, so server and browser render the same markup.

- 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, radio-group, textarea, tokens, utils.
npm packages: lucide-react@^1.67.0.

## Example

```tsx
"use client"

import * as React from "react"
import { ScheduleEditor, type ScheduleDraft } from "@/components/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. Defaults to when the editor first drew in the browser.
- `className?: string | undefined`

## Functions

### nextRuns

```ts
nextRuns(recurrence: ScheduleRecurrence, timeZone: string, from: Date, count = 5)
```

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.

## 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 (Monday to Friday) at {time}
- `ruleWeek`: Every {days} at {time}
- `ruleWeekNone`: No days chosen, 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.
- `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}
- `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 Monday to Friday at `time`. */
 | {
  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/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/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
```

## 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` (fixed strings) and `TermsProvider` (nouns); entity kinds and agent roles in `EntityKindsProvider` and `AgentRolesProvider`.
