You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
A schedule-triggered flow's notify delivers nothing on a multi-organization install: the run carries no organization, so the tenant-scoped inbox/delivery writes are refused (#8844) while the run reads healthy #16659
Measured on @objectstack/cli 17.3.0 (objectstack dev --fresh, memory AND sqlite drivers), in an app that seeds 12 employer organizations plus the platform one (objectstack-ai/ats, card F5/F6 — objectstack-ai/ats#7).
What happens
A type: 'schedule' flow (runAs: 'system', start node config.schedule: { type: 'cron', expression: '* * * * *' }) whose loop body ends in a notify node (channels: ['inbox'], recipients = user ids) runs on every tick, selects the right rows, and its update_record steps land. The notifications do not:
ScheduleTrigger launches the run with { event: 'schedule', params: { jobId, flowName, schedule } } and no tenantId (trigger-schedule/src/schedule-trigger.ts; the time-relative trigger threads none either).
sys_notification lands with organization_id = NULL, but the tenant-scoped rows below it do not: after two ticks on sqlite a direct read of the datastore shows sys_inbox_message EMPTY, every recipient's GET /api/v1/notifications is empty, and the run summary reports unmeasured=0 (the notify node's enqueued was 0), where the same flow triggered through POST /api/v1/automation/:name/trigger under a session reports unmeasured=4 and every recipient sees the row.
So on any install with more than one sys_organization, a scheduled flow that notifies is a green run that delivers nothing, and its history never persists. Record-change flows are unaffected (the triggering session's organization is threaded, and delivery works on both drivers).
Why the app cannot route around it
NotifyConfigSchema is strictObject and has no organization key, so a flow cannot hand notify the record's organization_id (an interview row carries one; a platform-global object such as a candidate credential — tenancy: { enabled: false } — carries none at all).
The run context is built by the trigger; a runAs: 'system' flow has no session to borrow from.
A defineJob handler receives { jobId, data, bundle, ql, logger } — no messaging handle — so moving the work to a job does not reach the inbox either.
Reproduction
Any stack with requires: ['automation', 'triggers', 'approvals', 'messaging'] and ≥ 2 organizations in sys_organization.
A schedule flow: get_record → loop → notify (inbox, user-id recipients). Cron * * * * * for the test.
Wait one tick; read GET /api/v1/notifications as a recipient (empty), and the boot log (no organization in scope + the REFUSED line for sys_automation_run).
Trigger the same flow via POST /api/v1/automation/<name>/trigger under a session: the recipient now sees the row.
Options seen from the app side (not a recommendation)
Let a scheduled trigger carry an acting organization (a start-node key, or a per-organization fan-out of the sweep).
Let notify accept an organization from the record (organizationId: '{record.organization_id}').
Declare the user-keyed inbox objects (sys_inbox_message, sys_notification_delivery, sys_notification_receipt) platform-global, since an inbox row belongs to a user, not to an organization — the refusal message itself suggests this route for rows that "belong to no organization".
Related, closed: #11303 (threaded the run's organization into notify — this is the trigger family that has none to thread), #14501, #10673.
Measured on
@objectstack/cli17.3.0 (objectstack dev --fresh, memory AND sqlite drivers), in an app that seeds 12 employer organizations plus the platform one (objectstack-ai/ats, card F5/F6 — objectstack-ai/ats#7).What happens
A
type: 'schedule'flow (runAs: 'system', start nodeconfig.schedule: { type: 'cron', expression: '* * * * *' }) whose loop body ends in anotifynode (channels: ['inbox'], recipients = user ids) runs on every tick, selects the right rows, and itsupdate_recordsteps land. The notifications do not:ScheduleTriggerlaunches the run with{ event: 'schedule', params: { jobId, flowName, schedule } }and notenantId(trigger-schedule/src/schedule-trigger.ts; the time-relative trigger threads none either).notifytherefore logs[notify] no organization in scope for topic '…'(the [finding] sys_inbox_message/sys_notification/sys_email 等平台表从不写 organization_id(存量与新增行 100% null)——请确认多组织语义是否设计如此 #11303 fail-loud limb) and emits with noorganizationId.sys_notificationlands withorganization_id = NULL, but the tenant-scoped rows below it do not: after two ticks on sqlite a direct read of the datastore showssys_inbox_messageEMPTY, every recipient'sGET /api/v1/notificationsis empty, and the run summary reportsunmeasured=0(the notify node'senqueuedwas 0), where the same flow triggered throughPOST /api/v1/automation/:name/triggerunder a session reportsunmeasured=4and every recipient sees the row.Insert on 'sys_automation_run' was REFUSED: a system-context write on a tenant-scoped object must carry an organization, and this install declares the 'single' tenancy posture but holds 2 organizations, so which one owns the row is not derivable— the System-context writes land untenanted at RUNTIME, so a single-tenant install keeps re-forking the autonumber scope and minting duplicate business identifiers — the producer #8686's backfill cannot reach (17.0.0 GA) #8844 Option 1 ruling (objectql/src/tenancy/system-write-organization.ts), whose own header names "every cron tick" as the producer it guards.So on any install with more than one
sys_organization, a scheduled flow that notifies is a green run that delivers nothing, and its history never persists. Record-change flows are unaffected (the triggering session's organization is threaded, and delivery works on both drivers).Why the app cannot route around it
NotifyConfigSchemaisstrictObjectand has no organization key, so a flow cannot handnotifythe record'sorganization_id(an interview row carries one; a platform-global object such as a candidate credential —tenancy: { enabled: false }— carries none at all).runAs: 'system'flow has no session to borrow from.defineJobhandler receives{ jobId, data, bundle, ql, logger }— no messaging handle — so moving the work to a job does not reach the inbox either.Reproduction
requires: ['automation', 'triggers', 'approvals', 'messaging']and ≥ 2 organizations insys_organization.scheduleflow:get_record→loop→notify(inbox, user-id recipients). Cron* * * * *for the test.GET /api/v1/notificationsas a recipient (empty), and the boot log (no organization in scope+ theREFUSEDline forsys_automation_run).POST /api/v1/automation/<name>/triggerunder a session: the recipient now sees the row.Options seen from the app side (not a recommendation)
notifyaccept an organization from the record (organizationId: '{record.organization_id}').sys_inbox_message,sys_notification_delivery,sys_notification_receipt) platform-global, since an inbox row belongs to a user, not to an organization — the refusal message itself suggests this route for rows that "belong to no organization".Related, closed: #11303 (threaded the run's organization into
notify— this is the trigger family that has none to thread), #14501, #10673.