actualbudget/actual · error

Schedule not found: ${id}

Error message

Schedule not found: ${id}

What it means

The schedule move/reorder logic verifies the schedule exists and is not tombstoned via SELECT id FROM schedules WHERE id = ? AND tombstone = 0; if no row is returned it throws 'Schedule not found: <id>'. Like the transaction move, it runs inside batchMessages so nothing is written when the check fails.

Source

Thrown at packages/loot-core/src/server/db/index.ts:997

    }
  });
}

/**
 * Move a schedule to a new position among all non-tombstoned schedules.
 * Uses the same midpoint/shove algorithm as transaction reordering.
 *
 * @param id - The ID of the schedule to move
 * @param targetId - The ID of the schedule to place AFTER, or null to place at top
 */
export async function moveSchedule(id: string, targetId: string | null) {
  await batchMessages(async () => {
    const schedule = await first<{ id: string }>(
      'SELECT id FROM schedules WHERE id = ? AND tombstone = 0',
      [id],
    );
    if (!schedule) {
      throw new Error(`Schedule not found: ${id}`);
    }

    const schedules = await all<{ id: string; sort_order: number }>(
      `SELECT id, sort_order FROM schedules
       WHERE tombstone = 0
       ORDER BY sort_order DESC, id`,
    );

    const { sort_order: newSortOrder, updates } = shoveSortOrdersDescending(
      schedules,
      targetId,
      id,
    );

    for (const info of updates) {
      await update('schedules', info);
    }

View on GitHub (pinned to d4334cb6e6)

Solutions

  1. Confirm the schedule id with getSchedule / query schedules before moving
  2. Re-sync so the client drops references to tombstoned schedules
  3. Remove the queued reorder operation if the schedule was intentionally deleted
  4. Ensure the id passed is a schedule id, not a rule or transaction id

Example fix

// before
await aqlQuery.moveSchedule(scheduleId, targetId);
// after
const sched = await aqlQuery.getSchedule(scheduleId);
if (sched) await aqlQuery.moveSchedule(scheduleId, targetId);
Defensive patterns

Strategy: try-catch

Validate before calling

const sched = await aqlQuery.getSchedule(scheduleId);
if (!sched) throw new Error(`Schedule ${scheduleId} no longer exists`);

Type guard

function scheduleExists(s: unknown): s is { id: string } {
  return !!s && typeof (s as any).id === 'string';
}

Try / catch

try {
  await aqlQuery.moveSchedule(scheduleId, targetId);
} catch (e) {
  if (e.message.startsWith('Schedule not found')) {
    removePendingOp('moveSchedule', scheduleId);
  } else throw e;
}

Prevention

When it happens

Trigger: Calling a schedule move/reorder API with an unknown id, a schedule deleted locally or on another device, or a stale id from client cache after a sync deleted the schedule.

Common situations: Two devices racing: one deletes a rule/schedule while the user on the other device reorders it; offline queue replaying reorder ops for removed schedules; typo'd or wrong entity id passed in.

Understand the failure class

Background: "Not found" and "does not exist" errors: why "Task not found", "No such folder", and "Can't find" fire when a lookup comes back empty — this error's family across 14 libraries.

Related errors


AI-assisted analysis of actualbudget/actual@d4334cb6e6 (2026-08-29). Data as JSON: /api/errors/7c9b391299fba1b4. Report an issue: GitHub.