---
title: Rename a cron group header
---

# Rename a cron group header

## What it is

The Cron Jobs view groups jobs two ways — **By Project** (each group header shows the project's name) or **By Frequency** (each group header shows the cadence: `hourly`, `daily`, `every-6-hours`, etc.). In project mode, the group header label can feel wrong: a project called "Agent Orchestrator" shows up as a long sidebar header, you may want it to read "Orchestrator" or "Omniscio" inside the cron view without renaming the project everywhere else, and orphan UUIDs from soft-deleted projects sometimes briefly surface as group headers before the next prune sweep cleans them up.

Group rename lets you override the label that the cron view uses for any project group **without touching the project's real name**. The override is local to your install (lives in `settings.cronGroupLabels`), applies in both the Cron Jobs view (`CronDashboard.tsx`) and the Cron Jobs sidebar (`CronJobsSidebar.tsx`), and is automatically reset back to the canonical project name when you clear it. Frequency-mode headers are **not renameable** — those labels are derived from the cron expression itself and renaming `hourly` to "every hour" would just confuse the next reader.

## Where to find it

The control lives in the Cron Jobs view — Omniscio's **Cron Jobs** virtual project in the sidebar —
and in the smaller cron sidebar attached to a session that owns cron jobs. In both places the jobs
are grouped either **By Project** or **By Frequency**, and the rename affordance appears only in
the By Project grouping, as a small pencil at the right edge of a group header when you hover it.

## How it behaves

### How to use it

1. **Open the Cron Jobs view or sidebar.** Either the **Cron Jobs** virtual project in the Omniscio sidebar, or the cron sidebar attached to a session that has cron jobs.
2. **Switch to By Project mode.** The segmented `By Project / By Frequency` control at the top of either surface controls grouping. Rename only appears in **By Project**.
3. **Hover the group header.** A small pencil icon fades in at the right edge of the row when you hover. The icon is also focus-revealed for keyboard users — `Tab` to the group header's row, then `Tab` again to the rename trigger.
4. **Click the pencil.** The header label is replaced by an inline text input pre-filled with the current label (the project name or whatever you previously renamed it to). The input auto-focuses and selects the existing text, so just type to overwrite.
5. **Save with Enter (or click outside).** Pressing Enter, or blurring the input by clicking elsewhere, commits the new label. Pressing **Esc** cancels without saving.
6. **Reset by saving an empty value.** To go back to the canonical project name, open the rename input, clear it (or just press Enter on whitespace), and commit. The override is removed and the header reverts to `project.name`. Saving the same value as the live label also resets — there's no way to "save the current value as an override".

The label is clamped to 80 characters and trimmed before save. Anything longer is silently truncated. The "Unassigned" group (jobs whose project no longer exists) is also renameable using the same flow, in case you want to label your orphan jobs as "To triage" or similar.

### What it doesn't do

- **Doesn't rename the project itself.** Other Omniscio views — the sessions sidebar, project switcher, breadcrumbs, settings — all keep showing the real `project.name`. To rename the project everywhere, use the project's three-dot menu → **Edit** → **Name**.
- **Doesn't sync across machines.** Overrides are per-install. If you use Omniscio on a desktop and a laptop, you'll need to rename groups on each.
- **Doesn't apply in By Frequency mode.** Frequency labels (`hourly`, `daily`, `weekly`, etc.) are derived and never overrideable. The pencil icon is hidden in frequency mode by design.
- **Doesn't survive project deletion.** When you delete a project (three-dot → Delete), Omniscio drops any cron group label override keyed by that project id as part of the cascade — so the override map stays in lockstep with the live project set. There's no "ghost label" left behind for an undelete to recover.

## For agents

### How it works

#### The override map

Overrides live in `settings.cronGroupLabels`, a `Record<string, string>` keyed by the **group key** (a project UUID, or the literal string `'Unassigned'` for orphan jobs). The schema is registered in `updateSettingsSchema` ([src/shared/ipc-schemas.ts](../../src/shared/ipc-schemas.ts)) as `z.record(z.string().min(1).max(64), z.string().min(1).max(80)).optional()` — empty-string values are rejected at IPC boundary, so the renderer must drop the key (not write `''`) when resetting.

#### The resolver — single source of truth

Both the dashboard and the sidebar look up labels through the same resolver hook, `useGroupLabelResolver(viewMode)`, in [src/renderer/src/features/cron/cron-group-label.ts](../../src/renderer/src/features/cron/cron-group-label.ts). The hook subscribes to the `projects` and `settings.cronGroupLabels` slices, then returns a `(groupKey) => { label, renameable }` closure that:

- In **frequency mode**, returns `{ label: groupKey, renameable: false }` — the cadence string is the label.
- In **project mode**, looks up the override first, then falls back to the live project name, then to a UUID-shortened orphan label (`abc12345…`) if neither exists. Always returns `renameable: true` in project mode — even orphans are renameable so you can name an unfamiliar UUID before prune sweeps it away.

The Unassigned sentinel is a hardcoded literal `'Unassigned'` shared between the renderer resolver and the cron-store producer ([cron-store.ts:408](../../src/renderer/src/stores/cron-store.ts)) — drift between the two would silently break the rename round-trip.

#### The header component

The pencil affordance and inline input live in [src/renderer/src/features/cron/CronJobGroupHeader.tsx](../../src/renderer/src/features/cron/CronJobGroupHeader.tsx). The component takes an optional `onRename: (newLabel: string) => void` prop — when omitted (frequency mode), the pencil and input are not rendered at all. When provided (project mode), the trigger button is hover/focus-revealed via a Tailwind `group-hover:opacity-100` chain and `e.stopPropagation()` on its onClick prevents the toggle button from collapsing the group while you click the pencil.

The input itself stops propagation on `onClick`/`onMouseDown` so clicking inside it doesn't toggle the group either. Enter/Escape are handled inside the input's `onKeyDown`; the input also commits on `onBlur`, so clicking outside the row saves the same as pressing Enter.

`handleCommit` runs the trim → slice-to-80 → "is this a reset?" check before calling `onRename`. The reset condition fires when the trimmed value is empty **or** equal to the trimmed live label — both cases call `onRename('')` so the wiring layer can delete the override key. Non-reset values pass through verbatim.

#### The wiring layer

`CronDashboard.tsx` and `CronJobsSidebar.tsx` each define their own `handleRenameGroup(groupKey, newLabel)` callback. Both follow the same shape:

1. Read the current `settings.cronGroupLabels` from the settings store (default `{}`).
2. If `newLabel === ''`, **delete** the key from a copy of the map (skip the IPC entirely if the key wasn't present — idempotent no-op).
3. Otherwise, **set** the key to the new label (skip the IPC if the value didn't change — idempotent no-op).
4. Call `useSettingsStore.getState().updateSetting('cronGroupLabels', updated)` — optimistic patch, rolls back on IPC error.

The two callbacks are intentionally duplicated rather than extracted into a shared helper. The shape is small enough (~10 lines), and centralizing it in `cron-group-label.ts` would create a renderer→store cycle in the resolver module that the linter would complain about.

#### Orphan key garbage collection

Two seams reap orphan keys (project UUIDs in the map that no longer correspond to a live project), both calling [`pruneOrphanCronGroupLabels()`](../../src/main/services/cron/cron-group-labels-service.ts):

1. **Startup hook** in [src/main/index.ts](../../src/main/index.ts), inside `app.whenReady()` after the AHK clear-stale block — runs once per launch, before the renderer paints, so any orphan that accumulated across sessions is gone before the user sees the cron view.
2. **Project delete cascade** in [src/main/services/project-delete-service.ts](../../src/main/services/project-delete-service.ts), immediately after the `ahkLinkedProjectId` clear — so the override map shrinks the moment a project leaves the live set.

The prune is idempotent: when no orphan keys exist, it's a no-op (no settings write, no log line). When it does prune, it logs `[cronGroupLabels] pruned N orphan keys (M remaining)`.

A soft-deleted project counts as missing — `queries.listProjects()` filters out `is_deleted = 1`, so the prune sees it as gone. This is intentional: undelete is rare enough that recovering a stale group label override would be more confusing than just letting the user re-rename.

### Where the data lives

| Layer    | File                                                                                                                 | Purpose                                                   |
| -------- | -------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------- |
| Type     | [src/shared/types.ts](../../src/shared/types.ts)                                                                     | `AppSettings.cronGroupLabels: Record<string, string>`     |
| Schema   | [src/shared/ipc-schemas.ts](../../src/shared/ipc-schemas.ts)                                                         | Zod `updateSettingsSchema` field — rejects empty values   |
| Resolver | [src/renderer/src/features/cron/cron-group-label.ts](../../src/renderer/src/features/cron/cron-group-label.ts)       | `useGroupLabelResolver(viewMode)` + `MAX_GROUP_LABEL_LEN` |
| Header   | [src/renderer/src/features/cron/CronJobGroupHeader.tsx](../../src/renderer/src/features/cron/CronJobGroupHeader.tsx) | Inline rename input + pencil affordance                   |
| Wiring   | [src/renderer/src/features/cron/CronDashboard.tsx](../../src/renderer/src/features/cron/CronDashboard.tsx)           | `handleRenameGroup` on the main view                      |
| Wiring   | [src/renderer/src/features/cron/CronJobsSidebar.tsx](../../src/renderer/src/features/cron/CronJobsSidebar.tsx)       | `handleRenameGroup` on the cron sidebar                   |
| GC       | [src/main/services/cron-group-labels-service.ts](../../src/main/services/cron/cron-group-labels-service.ts)          | `pruneOrphanCronGroupLabels()`                            |

### Tests

- [tests/unit/features/cron/cron-job-group-header.test.tsx](../../tests/unit/features/cron/cron-job-group-header.test.tsx) — 14 RTL tests covering frequency-mode parity, rename trigger render, input pre-fill, propagation isolation, Enter / Ctrl+Enter save, MAX_GROUP_LABEL_LEN clamp, empty / whitespace / equal-to-live → reset, ESC cancel, orphan group renameability.
- The resolver and prune service have their own unit tests — see the cron-group-label resolver test and the cron-group-labels-service test for the underlying behavior.

## Related

How cron jobs are created and edited in the first place is on the
[create a cron job with AI](create-cron-job-with-ai.md) page. What happens when one of them fails
is on the [cron failure alerts](cron-failure-alerts.md) page, and the session Omniscio can start to
repair a broken job automatically is on the [cron self-healing](cron-self-healing.md) page.
