Skip to content
Star on GitHub
WorkspaceScheduled runs
Style
Accent
Sound

Built by DesignMe, free to use under the MIT licence.

Expert design and dev support for startups and enterprises.

Book a call

Scheduled runs

Tasks the agent runs on a schedule, with their last result and a switch. Free and MIT licensed, as HTML or a typed React component, in two styles with light and dark mode.

$npx designme add scheduled-runs
Scheduled runs
01

Install

Add it to your project with the CLI.

HTML, CSS and JavaScript

$npx designme add scheduled-runs

React

$npx designme add scheduled-runs --react

The CLI copies the source into your project, so you own it and can change anything. The HTML version lands in ./designme with the shared core and a demo.html. The React version lands in src/components/designme and needs React 19; it works in Next.js, Vite and Astro.

02

About

What it is for and what it does.

A list of tasks an agent runs on a schedule, like a weekly design QA. Each row says its schedule in words, when it runs next and how its last run went, with Run now and an on/off switch. A run whose last attempt failed says why and can offer a fix, a paused one dims and says Paused, and New schedule makes another from Every day, Every Monday or Custom days.

  • When people set an agent to repeat work, like checking pages every week or syncing tokens every morning.
  • When people need to see at a glance which recurring runs passed and which failed.
  • When a run fails for a reason people can fix, like an expired token, and should run again once it is fixed.
  • When people want to pause a recurring run without deleting it.
03

Usage

Import it and drive it from your own state.

Import

import { ScheduledRuns } from '@/components/designme/scheduled-runs/ScheduledRuns';

Example

The demo above as a React component, 38 lines you can copy.

'use client';
// The animated preview from designme.dev, rebuilt from the React component: an example of driving it from your own state.
import { useRef, useState } from 'react';
import { useStory, useWait } from '../lib/useScript';
import { ScheduledRuns, type ScheduledRun } from './ScheduledRuns';

/* ---------- Scheduled runs: run the design QA now, reconnect Figma so the token sync passes, pause the price check ---------- */
const RUNS: ScheduledRun[] = [
  { key: 'qa', name: 'Weekly design QA', schedule: 'Mondays at 9:00', next: 'Next in 2 days', result: 'passed', ago: '5 days ago', icon: 'palette', color: 'var(--dm-pl)' },
  { key: 'tokens', name: 'Sync tokens from Figma', schedule: 'Every day at 8:00', next: 'Next in 17h', result: 'failed', error: 'Figma token expired', fix: 'Reconnect Figma', brand: 'figma' },
  { key: 'price', name: 'Competitor price check', schedule: 'Fridays at 17:00', next: 'Next in 6 days', result: 'passed', ago: 'yesterday', icon: 'chart', color: 'var(--dm-pt)' }
];

export default function ScheduledRunsDemo() {
  const ref = useRef<HTMLDivElement>(null);
  const [runs, setRuns] = useState(RUNS);
  const wait = useWait();
  const patch = (key: string, p: Partial<ScheduledRun>) => setRuns(rs => rs.map(r => r.key === key ? { ...r, ...p } : r));

  const runNow = async (key: string) => { patch(key, { result: 'running' }); await wait(3000); patch(key, { result: 'passed', ago: 'just now', error: undefined }); };
  // reconnecting takes a moment, then the run goes again straight away
  const fix = async (key: string) => { patch(key, { fixing: true }); await wait(900); patch(key, { fixing: false }); runNow(key); };

  // the preview's story: run the design QA and watch it pass, reconnect Figma so the token sync passes, then pause the price check
  useStory(ref, async ({ wait, press }) => {
    await wait(2000); press('.dm-shx-r:nth-child(1) .dm-shx-run');
    await wait(4000); press('.dm-shx-r:nth-child(2) .dm-shx-fx .dm-pill');
    await wait(5000); press('.dm-shx-r:nth-child(3) .dm-toggle');
  });

  return (
    <div ref={ref} style={{ width: '100%' }}>
      <ScheduledRuns runs={runs} onRunsChange={setRuns} onRunNow={runNow} onFix={fix}
        onCreate={n => setRuns(rs => [...rs, { key: 'new' + rs.length, name: n.task, schedule: n.schedule, next: n.next, result: 'never' }])} />
    </div>
  );
}
04

Props

Everything ScheduledRuns takes.

PropTypeWhat it does
runsScheduledRun[]
defaultRunsScheduledRun[]
onRunsChange(runs: ScheduledRun[]) => voidCalled when a switch pauses or resumes a run, with the whole list
onRunNow(key: string) => voidAdds Run now to each row. Set the run's result to 'running' while it runs
onFix(key: string) => voidCalled by the fix button of a failed run
onCreate(run: ScheduledRunsNew) => voidAdds New schedule: a task, Every day, Every Monday or Custom days, and Create
timestringThe time new schedules run at, in words. Defaults to "9:00"
hournumberThe hour of that time, so the form can say when the first run is. Defaults to 9
titlestringDefaults to "Scheduled runs"
statusstringThe line under the title. Defaults to "3 active · 1 failed · 1 paused", leaving out what is zero
classNamestring
05

Code

The full source, as the CLI installs it.

ScheduledRuns.tsxReact · 169 lines
'use client';
import { useRef, useState, type CSSProperties, type FormEvent } from 'react';
import { Brand, type BrandName } from '../lib/brands';
import { cx } from '../lib/cx';
import { useControllable, useIsoLayoutEffect } from '../lib/hooks';
import { Icon, type IconName } from '../lib/icons';
import { enter, Fold, Seg, Spin, Swap, Tip } from '../lib/motion';
import { usePopover } from '../lib/popover';
import '../designme.css';
import './scheduled-runs.css';

/** How its last run went. running while it runs now; never before its first run */
export type ScheduledRunResult = 'passed' | 'failed' | 'running' | 'never';

/** A task the agent runs on a schedule */
export interface ScheduledRun {
  key: string;
  /** Like "Weekly design QA" */
  name: string;
  /** The schedule in words, like "Mondays at 9:00" */
  schedule: string;
  /** When it runs next, like "Next in 2 days". A paused run says "Paused" instead */
  next?: string;
  /** On or paused. Defaults to on */
  enabled?: boolean;
  result?: ScheduledRunResult;
  /** When the last run ended, like "5 days ago", shown as "Passed 5 days ago" */
  ago?: string;
  /** Why the last run failed, like "Figma token expired" */
  error?: string;
  /** The label of a button that fixes a failed run, like "Reconnect Figma". It calls `onFix` */
  fix?: string;
  /** The fix is under way: its button spins */
  fixing?: boolean;
  /** A service's own logo. Wins over `icon` */
  brand?: BrandName;
  /** An icon on a tinted tile. Defaults to a clock */
  icon?: IconName;
  /** The icon's colour, which also tints its tile, like 'var(--dm-pl)' */
  color?: string;
}

/** What New schedule hands over: the task, and the days it runs on, 0 for Monday to 6 for Sunday */
export interface ScheduledRunsNew {
  task: string;
  repeat: 'day' | 'monday' | 'custom';
  days: number[];
  /** The schedule in words, like "Mon, Wed, Fri at 9:00" */
  schedule: string;
  /** When it first runs, from now, like "Next in 18h" */
  next: string;
}

export interface ScheduledRunsProps {
  runs?: ScheduledRun[];
  defaultRuns?: ScheduledRun[];
  /** Called when a switch pauses or resumes a run, with the whole list */
  onRunsChange?: (runs: ScheduledRun[]) => void;
  /** Adds Run now to each row. Set the run's result to 'running' while it runs */
  onRunNow?: (key: string) => void;
  /** Called by the fix button of a failed run */
  onFix?: (key: string) => void;
  /** Adds New schedule: a task, Every day, Every Monday or Custom days, and Create */
  onCreate?: (run: ScheduledRunsNew) => void;
  /** The time new schedules run at, in words. Defaults to "9:00" */
  time?: string;
  /** The hour of that time, so the form can say when the first run is. Defaults to 9 */
  hour?: number;
  /** Defaults to "Scheduled runs" */
  title?: string;
  /** The line under the title. Defaults to "3 active · 1 failed · 1 paused", leaving out what is zero */
  status?: string;
  className?: string;
}

const DAY = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'];
const FULL = ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday'];
const words = (ds: number[], time: string) => ds.length === 7 ? `Every day at ${time}` : ds.join() === '0,1,2,3,4' ? `Weekdays at ${time}`
  : ds.length === 1 ? `${FULL[ds[0]]}s at ${time}` : `${ds.map(d => DAY[d]).join(', ')} at ${time}`;
/** When the first run is, from now: in hours when it's within a day, otherwise in days */
function firstRun(ds: number[], hour: number) {
  const now = new Date(), today = (now.getDay() + 6) % 7;
  const at = ds.map(d => { const t = new Date(now); t.setDate(now.getDate() + ((d - today + 7) % 7)); t.setHours(hour, 0, 0, 0); if (t <= now) t.setDate(t.getDate() + 7); return t; })
    .sort((a, b) => +a - +b)[0];
  const h = Math.round((+at - +now) / 36e5);
  if (h < 24) return `First run in ${Math.max(1, h)}h`;
  const days = Math.round((new Date(at).setHours(0, 0, 0, 0) - new Date(now).setHours(0, 0, 0, 0)) / 864e5);
  return `First run in ${days} days`;
}

/** Recurring agent tasks: each with its schedule in words, when it runs next, how its last run went, Run now and an
    on/off switch. A paused one dims and says so; a failed one says why and can offer a fix; New schedule makes another. */
export function ScheduledRuns({ runs, defaultRuns = [], onRunsChange, onRunNow, onFix, onCreate, time = '9:00', hour = 9, title = 'Scheduled runs', status, className }: ScheduledRunsProps) {
  const [list, setList] = useControllable(runs, defaultRuns, onRunsChange);
  const root = useRef<HTMLDivElement>(null);
  const field = useRef<HTMLInputElement>(null);
  const menu = usePopover({ side: 'up' });
  const [task, setTask] = useState('');
  const [repeat, setRepeat] = useState<ScheduledRunsNew['repeat']>('day');
  const [custom, setCustom] = useState([0, 2, 4]);
  const days = repeat === 'day' ? [0, 1, 2, 3, 4, 5, 6] : repeat === 'monday' ? [0] : custom;
  // the form grows upwards from New schedule when Custom adds the days, so it is held by its bottom edge
  const [bottom, setBottom] = useState<CSSProperties>({});
  useIsoLayoutEffect(() => {
    const b = root.current?.querySelector<HTMLElement>('.dm-shx-add');
    if (menu.open && b && root.current) setBottom({ top: 'auto', bottom: root.current.clientHeight - b.offsetTop + 6 });
  }, [menu.open, repeat]);
  const [first] = useState(() => list.map(r => r.key));

  const on = (r: ScheduledRun) => r.enabled !== false;
  const active = list.filter(on).length, paused = list.length - active, failed = list.filter(r => on(r) && r.result === 'failed').length;
  const line = status ?? `${active} active` + (failed ? ` · ${failed} failed` : '') + (paused ? ` · ${paused} paused` : '');
  const toggle = (key: string) => setList(list.map(r => r.key === key ? { ...r, enabled: !on(r) } : r));
  const create = (e: FormEvent) => {
    e.preventDefault(); if (!task.trim() || !days.length) return;
    onCreate?.({ task: task.trim(), repeat, days, schedule: words(days, time), next: firstRun(days, hour).replace('First run', 'Next') });
    menu.close(); setTask('');
  };

  return (
    <div data-dm="" ref={root} className={cx('dm-shx dm-panel', className)}>
      <div className="dm-shx-h"><div><b>{title}</b><Swap className="dm-muted dm-shx-st">{line}</Swap></div></div>
      <div className="dm-shx-list" role="list" aria-label="Schedules">
        {list.map(r => {
          const at = first.indexOf(r.key), motion = enter(at >= 0 ? at * 50 : 0), running = r.result === 'running';
          const res = running ? 'Running now' : r.result === 'passed' ? 'Passed' + (r.ago ? ' ' + r.ago : '') : r.result === 'failed' ? 'Failed' + (r.error ? ': ' + r.error : '') : 'No runs yet';
          const st = running ? 'run' : r.result === 'passed' ? 'ok' : r.result === 'failed' ? 'bad' : 'none';
          const color = r.color ?? 'var(--dm-pb)';
          return (
            <div key={r.key} className={cx('dm-shx-r', motion.className, !on(r) && 'dm-off')} style={motion.style} role="listitem" data-st={st}>
              {r.brand
                ? <span className="dm-ic dm-brandic" aria-hidden="true"><Brand name={r.brand} /></span>
                : <span className="dm-ic" style={{ color, background: `color-mix(in srgb,${color} 12%,transparent)` }} aria-hidden="true"><Icon name={r.icon ?? 'clock'} size="xs" /></span>}
              <span className="dm-t">
                <span className="dm-nm">{r.name}</span>
                <Swap className="dm-d">{r.schedule + ' · ' + (on(r) ? r.next ?? '' : 'Paused')}</Swap>
                <span className="dm-res">{!running && (r.result === 'passed' || r.result === 'failed') && <Icon name={r.result === 'passed' ? 'ok' : 'alert'} size="xs" />}<Swap>{res}</Swap></span>
              </span>
              <span className="dm-shx-act">
                {onRunNow && <Tip label="Run now"><button type="button" className="dm-ib dm-shx-run" aria-label={`Run ${r.name} now`} disabled={running || !on(r)} onClick={() => onRunNow(r.key)}><Icon name="play" /></button></Tip>}
                <Tip label={on(r) ? 'Pause' : 'Resume'}><button type="button" className="dm-toggle" role="switch" aria-checked={on(r)} aria-label={r.name} onClick={() => toggle(r.key)} /></Tip>
              </span>
              <Fold open={r.result === 'failed' && !!r.fix} className="dm-shx-fx">
                <div><button type="button" className="dm-pill dm-accent dm-sm" onClick={() => !r.fixing && onFix?.(r.key)}>{r.fixing ? <><Spin />Reconnecting</> : r.fix}</button></div>
              </Fold>
            </div>
          );
        })}
      </div>
      {onCreate && <>
        <button type="button" className="dm-shx-add" {...menu.trigger} aria-haspopup="dialog" onClick={() => { menu.trigger.onClick(); if (!menu.open) requestAnimationFrame(() => field.current?.focus({ preventScroll: true })); }}><Icon name="plus" size="xs" />New schedule</button>
        <div {...menu.pop} style={{ ...menu.pop.style, ...bottom }} className={cx(menu.pop.className, 'dm-shx-pop')} role="dialog" aria-label="New schedule">
          <form className="dm-shx-f" onSubmit={create}>
            <h4>New schedule</h4>
            <input ref={field} className="dm-shx-in" value={task} onChange={e => setTask(e.target.value)} placeholder="What should the agent do?" aria-label="What should the agent do?" spellCheck={false} autoComplete="off" />
            <Seg className="dm-shx-rep" label="Repeat" value={repeat} onChange={setRepeat} options={[{ value: 'day', label: 'Every day' }, { value: 'monday', label: 'Every Monday' }, { value: 'custom', label: 'Custom' }]} />
            {repeat === 'custom' && (
              <div className="dm-shx-days" role="group" aria-label="Days">
                {DAY.map((d, i) => <button type="button" key={d} aria-pressed={custom.includes(i)} aria-label={FULL[i]} onClick={() => setCustom(c => c.includes(i) ? c.filter(x => x !== i) : [...c, i].sort())}>{d[0]}</button>)}
              </div>
            )}
            <div className="dm-shx-ff"><span className="dm-muted dm-shx-sum">{days.length ? firstRun(days, hour) : 'Pick at least one day'}</span><button type="submit" className="dm-pill dm-accent dm-sm" disabled={!task.trim() || !days.length}>Create</button></div>
          </form>
        </div>
      </>}
    </div>
  );
}
scheduled-runs.cssReact · 57 lines
/* Generated from designme.dev by scripts/sync.mjs. Edit the site, then sync. */
/* ---------- Scheduled runs ---------- */
/* recurring agent tasks: the schedule in words, when it runs next and how its last run went, with Run now and an on/off
   switch. A paused one dims and says so; one whose last run failed says why and offers the fix */
.dm-shx{padding:var(--dm-s-3);display:flex;flex-direction:column;gap:6px}
.dm-shx-h{display:flex;align-items:center;gap:10px;padding:2px 10px var(--dm-s-2)}
.dm-shx-h>div{display:flex;flex-direction:column;min-width:0}
.dm-shx-h b{font-weight:500;font-size:13.5px}
.dm-shx-st{font-size:12px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.dm-shx-list{display:flex;flex-direction:column}
.dm-shx-r{display:grid;grid-template-columns:28px minmax(0,1fr) auto;align-items:center;column-gap:var(--dm-s-3);padding:var(--dm-s-2) 10px;border-radius:var(--dm-r-in);font-size:13px;transition:background .2s}
@media (hover:hover) and (pointer:fine){.dm-shx-r:hover{background:var(--dm-row)}}
.dm-shx-r .dm-ic{width:28px;height:28px;border-radius:9px;display:grid;place-items:center;position:relative;align-self:center;transition:opacity .3s}
.dm-shx-r .dm-ic.dm-brandic{background:var(--dm-surface);box-shadow:inset 0 0 0 1px color-mix(in srgb,var(--dm-line) 50%,transparent);color:var(--dm-fg)}
.dm-shx-r .dm-ic .dm-brand{width:15px;height:15px}
.dm-shx-r .dm-ic::after{content:"";position:absolute;right:-2px;bottom:-2px;box-sizing:border-box;width:8px;height:8px;border-radius:50%;background:var(--dm-faint);box-shadow:0 0 0 2px var(--dm-surface);transition:background .3s}
.dm-shx-r[data-st="ok"] .dm-ic::after{background:var(--dm-ok)}
.dm-shx-r[data-st="bad"] .dm-ic::after{background:var(--dm-bad)}
.dm-shx-r[data-st="run"] .dm-ic::after{width:10px;height:10px;right:-3px;bottom:-3px;background:var(--dm-surface);border:1.5px solid var(--dm-accent-mark,var(--dm-accent));border-right-color:transparent;animation:dm-rot .8s linear infinite}
.dm-shx-r.dm-off .dm-ic::after{background:var(--dm-faint)}
/* the name shares its line with Run now and the switch; the schedule and the result below it take the full width */
.dm-shx-r .dm-ic{grid-row:1/4}
.dm-shx-r .dm-t{display:contents}
.dm-shx-r :is(.dm-nm,.dm-d,.dm-res){grid-column:2/-1;transition:color .3s}
.dm-shx-r .dm-nm{grid-column:2}
/* a paused one dims: its tile fades, and its name and result step down to muted, so the text stays readable */
.dm-shx-r.dm-off .dm-ic{opacity:.5}.dm-shx-r.dm-off :is(.dm-nm,.dm-res .dm-i){color:var(--dm-muted)}
.dm-shx-r .dm-nm,.dm-shx-r .dm-d{min-width:0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.dm-shx-r .dm-d{font-size:12px;color:var(--dm-muted)}
.dm-shx-r .dm-res{display:flex;align-items:center;gap:5px;min-width:0;height:18px;font-size:12px;color:var(--dm-muted)}
.dm-shx-r .dm-res>span{min-width:0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.dm-shx-r .dm-res .dm-i{color:var(--dm-ok)}
.dm-shx-r[data-st="bad"] .dm-res,.dm-shx-r[data-st="bad"] .dm-res .dm-i{color:var(--dm-bad)}
.dm-shx-act{grid-column:3;grid-row:1;display:flex;align-items:center;gap:var(--dm-s-2);margin-block:-5px}
.dm-shx-run{width:28px;height:28px}
.dm-shx-run:disabled{opacity:1;cursor:not-allowed;background:none;color:var(--dm-dis);transform:none}
.dm-shx-r .dm-shx-fx{grid-column:2/-1;grid-row:4}
.dm-shx-fx>div{overflow-clip-margin:0}
.dm-shx-fx>div>div{display:flex;flex-wrap:wrap;gap:6px;padding-top:var(--dm-s-2)}
.dm-shx-add{display:flex;align-items:center;gap:var(--dm-s-2);height:30px;padding:0 var(--dm-s-3) 0 10px;font-size:12.5px;color:var(--dm-fg2);border-radius:var(--dm-r-pill);align-self:flex-start;transition:background-color .2s,color .2s}
@media (hover:hover) and (pointer:fine){.dm-shx-add:hover{color:var(--dm-fg);background:var(--dm-row)}}
.dm-shx-pop{width:272px;padding:var(--dm-s-3)}
.dm-shx-f{display:flex;flex-direction:column;gap:var(--dm-s-2)}
.dm-shx-f h4{margin:0 0 2px;font-size:13px;font-weight:500}
.dm-shx-in{width:100%;height:32px;border:0;outline:0;border-radius:var(--dm-r-field);background:var(--dm-surface);box-shadow:inset 0 0 0 1px color-mix(in srgb,var(--dm-field-line) 50%,transparent);padding:0 10px;font:inherit;font-size:13px;color:var(--dm-fg);transition:box-shadow var(--dm-t-fast)}
@supports (corner-shape:squircle){.dm-shx-in{corner-shape:squircle;border-radius:var(--dm-r-field-sq)}}
.dm-shx-in::placeholder{color:var(--dm-muted)}
.dm-shx-in:focus{box-shadow:inset 0 0 0 1px color-mix(in srgb,var(--dm-field-line-strong) 50%,transparent)}
.dm-shx-rep{display:flex}
.dm-shx-rep button{flex:1 1 auto;padding:0 var(--dm-s-2)}
.dm-shx-days{display:grid;grid-template-columns:repeat(7,minmax(0,1fr));gap:var(--dm-s-1)}
.dm-shx-days button{height:28px;border-radius:var(--dm-r-sm);background:var(--dm-chip);font-size:12px;color:var(--dm-fg2);transition:background .2s,color .2s}
@media (hover:hover) and (pointer:fine){.dm-shx-days button:hover{background:var(--dm-hover);color:var(--dm-fg)}}
.dm-shx-days button[aria-pressed="true"]{background:var(--dm-accent-bg);color:var(--dm-accent-fg,var(--dm-accent))}
.dm-shx-ff{display:flex;align-items:center;gap:var(--dm-s-2);padding-top:2px}
.dm-shx-sum{flex:1;min-width:0;font-size:12px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
scheduled-runs.htmlHTML · 17 lines
<div class="card" data-c="scheduled-runs">
  <div class="stage-in">
    <div class="shx panel" data-reveal="flat">
      <div class="shx-h"><div><b>Scheduled runs</b><span class="muted shx-st" aria-live="polite"></span></div></div>
      <div class="shx-list" role="list" aria-label="Schedules"></div>
      <button class="shx-add" data-pop=".shx-pop" data-pop-pos="up" aria-haspopup="dialog" aria-expanded="false"><svg class="i xs"><use href="#i-plus"/></svg>New schedule</button>
      <div class="pop shx-pop" role="dialog" aria-label="New schedule"><form class="shx-f">
        <h4>New schedule</h4>
        <input class="shx-in" placeholder="What should the agent do?" aria-label="What should the agent do?" spellcheck="false" autocomplete="off">
        <div class="seg2 shx-rep" role="group" aria-label="Repeat"><button type="button" aria-pressed="true" data-v="day">Every day</button><button type="button" aria-pressed="false" data-v="mon">Every Monday</button><button type="button" aria-pressed="false" data-v="custom">Custom</button></div>
        <div class="shx-days" role="group" aria-label="Days" hidden></div>
        <div class="shx-ff"><span class="muted shx-sum"></span><button class="pill accent sm shx-mk" type="submit" disabled>Create</button></div>
      </form></div>
    </div>
  </div>
</div>
scheduled-runs.cssHTML · 56 lines
/* ---------- Scheduled runs ---------- */
/* recurring agent tasks: the schedule in words, when it runs next and how its last run went, with Run now and an on/off
   switch. A paused one dims and says so; one whose last run failed says why and offers the fix */
.shx{padding:var(--s-3);display:flex;flex-direction:column;gap:6px}
.shx-h{display:flex;align-items:center;gap:10px;padding:2px 10px var(--s-2)}
.shx-h>div{display:flex;flex-direction:column;min-width:0}
.shx-h b{font-weight:500;font-size:13.5px}
.shx-st{font-size:12px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.shx-list{display:flex;flex-direction:column}
.shx-r{display:grid;grid-template-columns:28px minmax(0,1fr) auto;align-items:center;column-gap:var(--s-3);padding:var(--s-2) 10px;border-radius:var(--r-in);font-size:13px;transition:background .2s}
@media (hover:hover) and (pointer:fine){.shx-r:hover{background:var(--row)}}
.shx-r .ic{width:28px;height:28px;border-radius:9px;display:grid;place-items:center;position:relative;align-self:center;transition:opacity .3s}
.shx-r .ic.brandic{background:var(--surface);box-shadow:inset 0 0 0 1px color-mix(in srgb,var(--line) 50%,transparent);color:var(--fg)}
.shx-r .ic .brand{width:15px;height:15px}
.shx-r .ic::after{content:"";position:absolute;right:-2px;bottom:-2px;box-sizing:border-box;width:8px;height:8px;border-radius:50%;background:var(--faint);box-shadow:0 0 0 2px var(--surface);transition:background .3s}
.shx-r[data-st="ok"] .ic::after{background:var(--ok)}
.shx-r[data-st="bad"] .ic::after{background:var(--bad)}
.shx-r[data-st="run"] .ic::after{width:10px;height:10px;right:-3px;bottom:-3px;background:var(--surface);border:1.5px solid var(--accent-mark,var(--accent));border-right-color:transparent;animation:rot .8s linear infinite}
.shx-r.off .ic::after{background:var(--faint)}
/* the name shares its line with Run now and the switch; the schedule and the result below it take the full width */
.shx-r .ic{grid-row:1/4}
.shx-r .t{display:contents}
.shx-r :is(.nm,.d,.res){grid-column:2/-1;transition:color .3s}
.shx-r .nm{grid-column:2}
/* a paused one dims: its tile fades, and its name and result step down to muted, so the text stays readable */
.shx-r.off .ic{opacity:.5}.shx-r.off :is(.nm,.res .i){color:var(--muted)}
.shx-r .nm,.shx-r .d{min-width:0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.shx-r .d{font-size:12px;color:var(--muted)}
.shx-r .res{display:flex;align-items:center;gap:5px;min-width:0;height:18px;font-size:12px;color:var(--muted)}
.shx-r .res>span{min-width:0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.shx-r .res .i{color:var(--ok)}
.shx-r[data-st="bad"] .res,.shx-r[data-st="bad"] .res .i{color:var(--bad)}
.shx-act{grid-column:3;grid-row:1;display:flex;align-items:center;gap:var(--s-2);margin-block:-5px}
.shx-run{width:28px;height:28px}
.shx-run:disabled{opacity:1;cursor:not-allowed;background:none;color:var(--dis);transform:none}
.shx-r .shx-fx{grid-column:2/-1;grid-row:4}
.shx-fx>div{overflow-clip-margin:0}
.shx-fx>div>div{display:flex;flex-wrap:wrap;gap:6px;padding-top:var(--s-2)}
.shx-add{display:flex;align-items:center;gap:var(--s-2);height:30px;padding:0 var(--s-3) 0 10px;font-size:12.5px;color:var(--fg2);border-radius:var(--r-pill);align-self:flex-start;transition:background-color .2s,color .2s}
@media (hover:hover) and (pointer:fine){.shx-add:hover{color:var(--fg);background:var(--row)}}
.shx-pop{width:272px;padding:var(--s-3)}
.shx-f{display:flex;flex-direction:column;gap:var(--s-2)}
.shx-f h4{margin:0 0 2px;font-size:13px;font-weight:500}
.shx-in{width:100%;height:32px;border:0;outline:0;border-radius:var(--r-field);background:var(--surface);box-shadow:inset 0 0 0 1px color-mix(in srgb,var(--field-line) 50%,transparent);padding:0 10px;font:inherit;font-size:13px;color:var(--fg);transition:box-shadow var(--t-fast)}
@supports (corner-shape:squircle){.shx-in{corner-shape:squircle;border-radius:var(--r-field-sq)}}
.shx-in::placeholder{color:var(--muted)}
.shx-in:focus{box-shadow:inset 0 0 0 1px color-mix(in srgb,var(--field-line-strong) 50%,transparent)}
.shx-rep{display:flex}
.shx-rep button{flex:1 1 auto;padding:0 var(--s-2)}
.shx-days{display:grid;grid-template-columns:repeat(7,minmax(0,1fr));gap:var(--s-1)}
.shx-days button{height:28px;border-radius:var(--r-sm);background:var(--chip);font-size:12px;color:var(--fg2);transition:background .2s,color .2s}
@media (hover:hover) and (pointer:fine){.shx-days button:hover{background:var(--hover);color:var(--fg)}}
.shx-days button[aria-pressed="true"]{background:var(--accent-bg);color:var(--accent-fg,var(--accent))}
.shx-ff{display:flex;align-items:center;gap:var(--s-2);padding-top:2px}
.shx-sum{flex:1;min-width:0;font-size:12px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
scheduled-runs.jsHTML · 79 lines
DM.def('scheduled-runs', (r, c) => {
  r.innerHTML = r.innerHTML.replace('{plus}', I('plus','xs'));
  // in this preview it is Saturday afternoon, so Monday at 9:00 is two days off, and every new schedule runs at 9:00
  const R = [
    { mark:'palette', col:'var(--pl)', name:'Weekly design QA', when:'Mondays at 9:00', next:'Next in 2 days', on:true, res:'ok', ago:'5 days ago' },
    { mark:'@figma', name:'Sync tokens from Figma', when:'Every day at 8:00', next:'Next in 17h', on:true, res:'bad', why:'Figma token expired', fix:'Reconnect Figma' },
    { mark:'chart', col:'var(--pt)', name:'Competitor price check', when:'Fridays at 17:00', next:'Next in 6 days', on:true, res:'ok', ago:'yesterday' }
  ];
  const list = $('.shx-list', r), st = $('.shx-st', r), add = $('.shx-add', r), pop = $('.shx-pop', r), form = $('.shx-f', r);
  const inp = $('.shx-in', r), days = $('.shx-days', r), sum = $('.shx-sum', r), mk = $('.shx-mk', r);
  function row(x){
    const ic = x.mark[0] === '@' ? `<span class="ic brandic" aria-hidden="true">${BRAND[x.mark.slice(1)]}</span>` : `<span class="ic" style="color:${x.col};background:color-mix(in srgb,${x.col} 12%,transparent)" aria-hidden="true">${I(x.mark,'xs')}</span>`;
    x.el = html(`<div class="shx-r" role="listitem">${ic}<span class="t"><span class="nm"></span><span class="d"></span><span class="res"></span></span><span class="shx-act"><button class="ib shx-run" data-tip="Run now">${I('play')}</button><button class="toggle" role="switch"></button></span><div class="shx-fx fold shut" inert><div><div><button class="pill accent sm shx-fix"></button></div></div></div></div>`);
    $('.nm', x.el).textContent = x.name; $('.shx-run', x.el).setAttribute('aria-label', 'Run ' + x.name + ' now'); $('.toggle', x.el).setAttribute('aria-label', x.name);
    $('.shx-run', x.el).onclick = () => run(x);
    $('.toggle', x.el).onclick = () => { x.on = !x.on; paint(x); counts(); };
    $('.shx-fix', x.el).onclick = () => fix(x);
    paint(x, true); return x.el;
  }
  function paint(x, now){
    const el = x.el, put = (e, t, h) => now ? (h ? e.innerHTML = t : e.textContent = t) : swap(e, t, h);
    el.classList.toggle('off', !x.on); el.dataset.st = x.run ? 'run' : x.res || 'none';
    const tg = $('.toggle', el); tg.setAttribute('aria-checked', x.on); tg.dataset.tip = x.on ? 'Pause' : 'Resume';
    put($('.d', el), x.when + ' · ' + (x.on ? x.next : 'Paused'));
    const res = document.createElement('span'); res.textContent = x.run ? 'Running now' : x.res === 'ok' ? 'Passed ' + x.ago : x.res === 'bad' ? 'Failed: ' + x.why : 'No runs yet';
    put($('.res', el), (!x.run && x.res ? I(x.res === 'ok' ? 'ok' : 'alert', 'xs') : '') + res.outerHTML, true);
    $('.shx-run', el).disabled = !!x.run || !x.on;
    const fx = $('.shx-fx', el), on = x.res === 'bad' && !x.run, b = $('.shx-fix', el);
    if (on && !x.fixing) b.textContent = x.fix;
    fx.classList.toggle('shut', !on); fx.inert = !on;
  }
  function counts(now){
    const on = R.filter(x => x.on).length, off = R.length - on, bad = R.filter(x => x.on && x.res === 'bad' && !x.run).length;
    const t = `${on} active` + (bad ? ` · ${bad} failed` : '') + (off ? ` · ${off} paused` : '');
    if (now) st.textContent = t; else swap(st, t);
  }
  // Run now: the dot on its icon turns while it runs, then it passes
  function run(x){
    if (x.run) return; x.run = true; paint(x); counts();
    c.later(3000, () => { x.run = false; x.res = 'ok'; x.ago = 'just now'; paint(x); counts(); });
  }
  // the fix for a failed run reconnects, then runs it again straight away
  function fix(x){
    if (x.fixing) return; x.fixing = true; $('.shx-fix', x.el).innerHTML = '<span class="spin"></span>Reconnecting';
    c.later(900, () => { x.fixing = false; run(x); });
  }
  R.forEach(x => list.appendChild(row(x))); stagger([...list.children], 50); counts(true);
  // New schedule: a task in words and how often. Custom picks the days; the line under it says when the first run is
  const DAY = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'], FULL = ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday'];
  days.innerHTML = DAY.map((d, i) => `<button type="button" aria-pressed="${i === 0 || i === 2 || i === 4}" aria-label="${FULL[i]}">${d[0]}</button>`).join('');
  let rep = 'day';
  const picked = () => rep === 'day' ? [0, 1, 2, 3, 4, 5, 6] : rep === 'mon' ? [0] : $$('button', days).map((b, i) => b.getAttribute('aria-pressed') === 'true' ? i : -1).filter(i => i >= 0);
  const when = ds => ds.length === 7 ? 'Every day at 9:00' : ds.join() === '0,1,2,3,4' ? 'Weekdays at 9:00' : ds.length === 1 ? FULL[ds[0]] + 's at 9:00' : ds.map(d => DAY[d]).join(', ') + ' at 9:00';
  // today is Saturday (5) after 9:00, so tomorrow's run is 18 hours off and the rest whole days
  const next = ds => { const n = Math.min(...ds.map(d => (d - 5 + 7) % 7 || 7)); return n === 1 ? 'Next in 18h' : `Next in ${n} days`; };
  function form1(){ const ds = picked(); sum.textContent = ds.length ? next(ds).replace('Next', 'First run') : 'Pick at least one day'; mk.disabled = !inp.value.trim() || !ds.length; }
  seg($('.shx-rep', r), v => { rep = v; days.hidden = v !== 'custom'; form1(); if (pop.classList.contains('open')) placePop(add, pop); });
  $$('button', days).forEach(b => b.onclick = () => { b.setAttribute('aria-pressed', b.getAttribute('aria-pressed') !== 'true'); form1(); });
  inp.oninput = form1; form1();
  c.on(add, 'click', e => { if (!pop.classList.contains('open') && e.isTrusted) c.later(0, () => inp.focus({ preventScroll:true })); });
  c.on(form, 'submit', e => {
    e.preventDefault(); const ds = picked(); if (!inp.value.trim() || !ds.length) return;
    const x = { mark:'clock', col:'var(--pb)', name:inp.value.trim(), when:when(ds), next:next(ds), on:true, res:null };
    R.push(x); list.appendChild(row(x)); enter(x.el); counts(); closePops(); inp.value = ''; form1();
  });
  // the preview's story: run the design QA now and watch it pass, reconnect Figma so the token sync passes again, then
  // pause the price check
  c.story(async s => {
    const [qa, fig, price] = R;
    await s.beat(3);
    await s.point($('.shx-run', qa.el)); await s.beat(1);
    await s.tap($('.shx-run', qa.el)); s.away();
    await s.until(() => !qa.run && qa.ago === 'just now'); await s.beat(2);
    await s.tap($('.shx-fix', fig.el)); s.away();
    await s.until(() => !fig.run && fig.res === 'ok'); await s.beat(2);
    await s.tap($('.toggle', price.el)); s.away(); await s.beat(3);
  });
});
06

Questions

What people ask about it.

Does Run now start the run?
No. It calls onRunNow with the run's key. Start it in your own code, set its result to 'running', and to 'passed' or 'failed' when it ends.
How do I show a fix like reconnecting a service?
Give the failed run a fix label, like "Reconnect Figma". When onFix is called, set fixing while you reconnect, then run it again and update its result.
What time do new schedules run at?
At time, 9:00 unless you change it, with hour set to match so the form can say when the first run is. The days come back as numbers, 0 for Monday to 6 for Sunday.
Can I write schedules my own way?
Yes. schedule and next are plain text, so "Weekdays at 8:30" or "Next tomorrow" work as well.
07

More in Workspace

Navigation, search, settings, models and automations around the agent.

Automation canvasA trigger, a few agent steps, and a run you can watch flow through.
Canvas toolbarA floating toolbar for a workflow canvas: run, pick a tool, comment, fit and zoom.
Command paletteJump anywhere or hand a task to the agent from one box.
Memory panelWhat the agent remembers about your team, editable and removable.
Model compareRun one prompt on three models side by side and keep the best.
Agent settingsHow much the agent may do alone, with a save bar for changes.
Tools and connectionsThe tools the agent can reach, and how much it may do with each.
Knowledge sourcesThe files, sites and folders the agent knows, and how far each is indexed.
All 11 in Workspace →