WorkspaceScheduled runs
Built by DesignMe, free to use under the MIT licence.
Expert design and dev support for startups and enterprises.
Book a callScheduled 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.
Install
Add it to your project with the CLI.
HTML, CSS and JavaScript
npx designme add scheduled-runsReact
npx designme add scheduled-runs --reactThe 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.
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.
- Each run in
runsshows itsname, itsschedulein words with itsnextrun, and its lastresult: "Passed 5 days ago", "Failed: Figma token expired", "Running now" or "No runs yet". - The dot on its icon follows the result: green when it passed, red when it failed, a turning ring while it runs, grey when it is paused or has never run. Its tile shows a service logo (
brand) or aniconon a tint of itscolor. onRunNowadds a Run now button to each row; it is unavailable while the run is running or paused.- The switch pauses and resumes a run (
enabled), and the change comes back throughonRunsChange. A paused row dims and reads "Paused" in place of its next run. - A failed run with a
fixshows a button with that label that callsonFix; setfixingto spin it. onCreateadds New schedule: a form with the task, Every day, Every Monday or Custom days, and a line saying when the first run would be. Create hands over the task, the days, the schedule in words and when it runs next.- The line under the title reads like "3 active · 1 failed", adding "1 paused" when one is paused, or shows your own
status.
- The runs form a list. Each Run now button is named for its run, like "Run Weekly design QA now".
- Each switch has role="switch" and aria-checked, is named for its run, and has a Pause or Resume tooltip.
- New schedule opens a dialog; its Repeat options are a labelled group of pressed buttons, and the Custom days are buttons named in full, like "Wednesday".
- The line under the title is announced politely when it changes.
- With reduced motion, rows appear without their entrance and text changes without the crossfade.
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>
);
}
Props
Everything ScheduledRuns takes.
| Prop | Type | What it does |
|---|---|---|
runs | ScheduledRun[] | |
defaultRuns | ScheduledRun[] | |
onRunsChange | (runs: ScheduledRun[]) => void | Called when a switch pauses or resumes a run, with the whole list |
onRunNow | (key: string) => void | Adds Run now to each row. Set the run's result to 'running' while it runs |
onFix | (key: string) => void | Called by the fix button of a failed run |
onCreate | (run: ScheduledRunsNew) => void | Adds New schedule: a task, Every day, Every Monday or Custom days, and Create |
time | string | The time new schedules run at, in words. Defaults to "9:00" |
hour | number | The hour of that time, so the form can say when the first run is. Defaults to 9 |
title | string | Defaults to "Scheduled runs" |
status | string | The line under the title. Defaults to "3 active · 1 failed · 1 paused", leaving out what is zero |
className | string |
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);
});
});
Questions
What people ask about it.
- Does Run now start the run?
- No. It calls
onRunNowwith the run's key. Start it in your own code, set itsresultto 'running', and to 'passed' or 'failed' when it ends. - How do I show a fix like reconnecting a service?
- Give the failed run a
fixlabel, like "Reconnect Figma". WhenonFixis called, setfixingwhile you reconnect, then run it again and update itsresult. - What time do new schedules run at?
- At
time, 9:00 unless you change it, withhourset 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.
scheduleandnextare plain text, so "Weekdays at 8:30" or "Next tomorrow" work as well.
More in Workspace
Navigation, search, settings, models and automations around the agent.