clockworklabs/SpacetimeDB · error · TypeError
Schedule target table is registered more than once in this…
Error message
Schedule target table is registered more than once in this schema. Use a distinct table handle for each scheduled table.
What it means
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.
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.
Example fix
// before
const messages = table({}, messageRow);
export default schema({ messages, messagesV2: messages }); // same handle twice
// after
const messages = table({}, messageRow);
const messagesV2 = table({}, messageRow); // distinct handle for the second entry
export default schema({ messages, messagesV2 }); Defensive patterns
Strategy: validation
Validate before calling
// Ensure scheduled tables are distinct objects: register each entry once.
function assertDistinctTables(entries: Record<string, unknown>) {
const seen = new Set<object>();
for (const [name, entry] of Object.entries(entries)) {
if (typeof entry === 'object' && entry !== null) {
if (seen.has(entry)) throw new Error(`table object registered twice: ${name}`);
seen.add(entry);
}
}
} Prevention
- 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.
When it happens
Trigger: 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.
Common situations: 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.
Related errors
- Schedule target table is not part of this schema.
- Table defines a schedule, but it does not have a ScheduleAt…
- Table defines multiple schedules: and . A schedule table…
- invalid sequence type
- multiple schemas are not supported
AI-assisted analysis of clockworklabs/SpacetimeDB@eddf9f5014 (2026-08-20).
Data as JSON: /api/errors/338fe8bd86dfb055.
Report an issue: GitHub.
Appendix: source
Thrown at crates/bindings-typescript/src/server/schema.ts:164
// Pending schedules come from two API paths:
// - legacy table({ scheduled }) schedules already know their tableName and
// scheduleAtCol because schema() resolves them while iterating table keys
// - reducer/procedure({ onSchedule }) schedules only know the table handle,
// so resolve them through tableSourceNames and reject duplicate table
// handle registrations because the target table name is ambiguous
const scheduledTables = new Map<string, string>();
for (const {
functionName: knownFunctionName,
reducer,
table,
tableName: knownTableName,
scheduleAtCol: knownScheduleAtCol,
} of this.pendingSchedules) {
let tableName = knownTableName;
if (tableName === undefined) {
const tableNames = this.tableSourceNames.get(table);
if (tableNames !== undefined && tableNames.length > 1) {
throw new TypeError(
'Schedule target table is registered more than once in this schema. Use a distinct table handle for each scheduled table.'
);
}
tableName = tableNames?.[0];
}
if (tableName === undefined) {
throw new TypeError(
'Schedule target table is not part of this schema.'
);
}
const scheduleAtCol = knownScheduleAtCol ?? table.scheduleAtCol;
if (scheduleAtCol === undefined) {
throw new TypeError(
`Table ${tableName} defines a schedule, but it does not have a ScheduleAt column.`
);
}
View on GitHub (pinned to eddf9f5014)