{"record":{"id":"0fb09c52fd7a468e","repo":"actualbudget/actual","slug":"schedule-not-attached-to-a-rule","errorCode":null,"errorMessage":"Schedule not attached to a rule","messagePattern":"Schedule not attached to a rule","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"packages/loot-core/src/server/schedules/app.ts","lineNumber":176,"sourceCode":"    if (\n      action.op === 'set' &&\n      action.field === 'amount' &&\n      !action.options?.template &&\n      !action.options?.formula &&\n      action.value !== amount\n    ) {\n      changed = true;\n      return { ...action, value: amount };\n    }\n    return action;\n  });\n\n  return changed ? updated : null;\n}\n\nexport async function getRuleForSchedule(id: string | null): Promise<Rule> {\n  if (id == null) {\n    throw new Error('Schedule not attached to a rule');\n  }\n\n  const { data: ruleId } = await aqlQuery(\n    q('schedules').filter({ id }).calculate('rule'),\n  );\n  return getRules().find(rule => rule.id === ruleId);\n}\n\nasync function fixRuleForSchedule(id) {\n  const { data: ruleId } = await aqlQuery(\n    q('schedules').filter({ id }).calculate('rule'),\n  );\n\n  if (ruleId) {\n    // Take the bad rule out of the system so it never causes problems\n    // in the future\n    await db.delete_('rules', ruleId);\n  }","sourceCodeStart":158,"sourceCodeEnd":194,"githubUrl":"https://github.com/actualbudget/actual/blob/d4334cb6e6123f4d3bcea1ad6166608884c7e658/packages/loot-core/src/server/schedules/app.ts#L158-L194","documentation":"`getRuleForSchedule` requires the schedule to have an attached rule id. It throws synchronously when called with `null`/`undefined` id, and the underlying lookup is only valid for schedules created through `createSchedule`, which always creates a backing rule.","triggerScenarios":"Calling `getRuleForSchedule(null)` or `getRuleForSchedule(undefined)`; also reachable via `updateSchedule`/`setNextDate` when the schedule's `rule` column is null in the database.","commonSituations":"Manually inserted/corrupted schedule rows lacking a rule reference; calling schedule APIs on a schedule id that was partially deleted.","solutions":["Pass a valid non-null schedule id","Verify the schedule row has a non-null `rule` column in the schedules table","Recreate the schedule (createSchedule) if its backing rule is missing","Check `getRules()` includes the rule (rules loaded) before lookup"],"exampleFix":"// before\nconst rule = await getRuleForSchedule(schedule.rule); // schedule.rule is null\n// after\nif (schedule.rule == null) {\n  throw new Error('Schedule ' + schedule.id + ' has no attached rule');\n}\nconst rule = await getRuleForSchedule(schedule.rule);","handlingStrategy":"type-guard","validationCode":"if (schedule == null || schedule.rule == null) {\n  throw new Error('Schedule has no attached rule; recreate it via createSchedule');\n}\nconst rule = await getRuleForSchedule(schedule.rule);","typeGuard":"function hasAttachedRule(schedule) {\n  return schedule != null && typeof schedule.id === 'string' &&\n    schedule.rule != null && typeof schedule.rule === 'string';\n}","tryCatchPattern":"try {\n  const rule = await getRuleForSchedule(id);\n} catch (e) {\n  if (e.message === 'Schedule not attached to a rule') {\n    logger.warn('Schedule', id, 'has no rule — skipping');\n    return null;\n  }\n  throw e;\n}","preventionTips":["Only create schedules through createSchedule so a backing rule always exists","Never null out the schedules.rule column manually","Check schedule.rule != null before any rule-dependent schedule API","When deleting rules, delete or detach their schedules too"],"tags":["schedule","null-argument","data-integrity"],"backgroundTag":"schedule-missing-rule","analyzedSha":"d4334cb6e6123f4d3bcea1ad6166608884c7e658","analyzedAt":"2026-08-29T01:02:11.213Z","schemaVersion":2},"datasetVersion":"2026-08-29T02:17:18.158Z"}