{"record":{"id":"338fe8bd86dfb055","repo":"clockworklabs/SpacetimeDB","slug":"schedule-target-table-is-registered-more-than-once","errorCode":null,"errorMessage":"Schedule target table is registered more than once in this schema. Use a distinct table handle for each scheduled table.","messagePattern":"Schedule target table is registered more than once in this schema\\. Use a distinct table handle for each scheduled table\\.","errorType":"exception","errorClass":"TypeError","httpStatus":null,"severity":"error","filePath":"crates/bindings-typescript/src/server/schema.ts","lineNumber":164,"sourceCode":"    // Pending schedules come from two API paths:\n    // - legacy table({ scheduled }) schedules already know their tableName and\n    //   scheduleAtCol because schema() resolves them while iterating table keys\n    // - reducer/procedure({ onSchedule }) schedules only know the table handle,\n    //   so resolve them through tableSourceNames and reject duplicate table\n    //   handle registrations because the target table name is ambiguous\n    const scheduledTables = new Map<string, string>();\n    for (const {\n      functionName: knownFunctionName,\n      reducer,\n      table,\n      tableName: knownTableName,\n      scheduleAtCol: knownScheduleAtCol,\n    } of this.pendingSchedules) {\n      let tableName = knownTableName;\n      if (tableName === undefined) {\n        const tableNames = this.tableSourceNames.get(table);\n        if (tableNames !== undefined && tableNames.length > 1) {\n          throw new TypeError(\n            'Schedule target table is registered more than once in this schema. Use a distinct table handle for each scheduled table.'\n          );\n        }\n        tableName = tableNames?.[0];\n      }\n      if (tableName === undefined) {\n        throw new TypeError(\n          'Schedule target table is not part of this schema.'\n        );\n      }\n\n      const scheduleAtCol = knownScheduleAtCol ?? table.scheduleAtCol;\n      if (scheduleAtCol === undefined) {\n        throw new TypeError(\n          `Table ${tableName} defines a schedule, but it does not have a ScheduleAt column.`\n        );\n      }\n","sourceCodeStart":146,"sourceCodeEnd":182,"githubUrl":"https://github.com/clockworklabs/SpacetimeDB/blob/eddf9f5014579a50d4b67630e28b6e15cad9c4af/crates/bindings-typescript/src/server/schema.ts#L146-L182","documentation":"When resolving scheduled functions, the schema maps each schedule target table OBJECT to the source names it was registered under (`tableSourceNames`). If no explicit name was given for the schedule and the same table handle object was registered under more than one schema entry, the runtime cannot decide which name the schedule belongs to, so it throws and tells you to use a distinct handle per scheduled table. `table()` must be called separately for each scheduled table.","triggerScenarios":"Creating one table handle with `const msgs = table({}, {...})` and listing it under two entries (`{ msgs, archive: msgs }`), then scheduling a function on it; reusing a single `table()` result for both a main entry and a legacy `schedule()` registration; sharing a module-level table constant between schema entries.","commonSituations":"Alias entries in the schema object meant as 'convenience accessors'; refactoring that hoists a table into a shared constant referenced twice; migrating from the legacy schedule API while keeping the old entry.","solutions":["Give each scheduled table its own handle: call `table(...)` a second time (fresh object) for the second entry instead of reusing the same instance.","Remove alias entries that point at the same table object; access the table through the one canonical entry.","If you use the legacy `table.schedule` API, pass the explicit table name so name inference (`knownTableName`) is not needed."],"exampleFix":"// before\nconst messages = table({}, messageRow);\nexport default schema({ messages, messagesV2: messages }); // same handle twice\n\n// after\nconst messages = table({}, messageRow);\nconst messagesV2 = table({}, messageRow); // distinct handle for the second entry\nexport default schema({ messages, messagesV2 });","handlingStrategy":"validation","validationCode":"// Ensure scheduled tables are distinct objects: register each entry once.\nfunction assertDistinctTables(entries: Record<string, unknown>) {\n  const seen = new Set<object>();\n  for (const [name, entry] of Object.entries(entries)) {\n    if (typeof entry === 'object' && entry !== null) {\n      if (seen.has(entry)) throw new Error(`table object registered twice: ${name}`);\n      seen.add(entry);\n    }\n  }\n}","typeGuard":null,"tryCatchPattern":null,"preventionTips":["Call `table()` once per schema entry; never alias one handle into two entries.","Treat schema entry objects as declaration sites — no convenience aliases.","When wrapping legacy schedule APIs, pass the explicit table name so inference is unnecessary."],"tags":["scheduling","schema","tables","build-time","spacetimedb"],"backgroundTag":"table-registered-twice","analyzedSha":"eddf9f5014579a50d4b67630e28b6e15cad9c4af","analyzedAt":"2026-08-20T06:08:37.179Z","contentChangedAt":"2026-08-20T06:08:37.179Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}