ObjectStackObjectStack

Schedule Organization — Automation Protocol

The ACTING ORGANIZATION of a time-triggered flow — the one start-node key that says which organization a scheduled run executes as.

The ACTING ORGANIZATION of a time-triggered flow — the one start-node key that says which organization a scheduled run executes as.

Why the key exists

A record-change flow inherits its organization from the write that fired it: the triggering session's tenantId rides the AutomationContext into the run, so every tenant-scoped write below it — sys_inbox_message, sys_notification_delivery, sys_automation_run — resolves an organization the way a session write does. A TIME-triggered flow has no such session. The schedule trigger and the time-relative sweep launch their runs from a job tick, and a job tick carries no identity at all, so the run reached the tenancy guard (system-write-organization.ts) with nothing to offer it. On an install holding more than one sys_organization that guard refuses, correctly and by design — and the refusal landed on rows the run never reported: the notification wrote with organization_id = NULL, every tenant-scoped row beneath it was refused, and the tick still summarised itself as healthy.

The ruling this key implements

Maintainer, 2026-09-08, verbatim:

多组织定时任务本来只能在组织内运行,应该带组织ID,不允许跨组织的定时任务。

Under the isolated tenancy posture a time-triggered flow is organization-scoped by construction: it names one organization and the run executes as that organization. There is deliberately no fan-out — a tenant that wants the same sweep in N organizations declares it N times — and there is deliberately no fallback: a flow that names none is a DECLARATION ERROR, not a run that quietly picks one. Guessing is the failure this key exists to prevent, and the platform organization is not a safe guess: a wrong organization_id is worse than a null, because a null is visibly missing while a wrong value is silently authoritative to every report, export and cleanup script that filters by organization.

Where that requirement bites, and where it does not (#17396, #18378)

⚠️ The sentence above is scoped by POSTURE, and that scoping is the whole of the 2026-09-12 and 2026-09-16 amendments. Two deployment facts decide whether this key is required, and ⛔ neither of them is metadata — both are read from the environment at boot, beside resolveTenancyPosture:

deploymentis this key required?where a bound run's writes get their organization
package-authored scheduled work switched OFF (the global default)⛔ nothing arms, so nothing is required — the flow is listed as disabled by deployment policy, never as a binding failure—
switched ON, posture singlenothe run carries none; every tenant-scoped insert beneath it resolves the install's one organization through the #8844 guard
switched ON, posture groupno — optionaldeclared ⇒ the declaration, bounding SELECTION and identity alike; undeclared ⇒ the swept record's own organization
switched ON, posture isolatedyes — declare or the flow is not armedthe declaration

⇒ The key is never deprecated and its meaning never changes: where a run declares one, that is the organization it acts as, and nothing on this path ever chooses one out of the air. What changed is that a missing key is no longer a defect on every deployment — so ⛔ do not read the refusal sentence below as a universal authoring rule, and ⛔ do not re-add an authoring-time lint for it: at authoring time neither the switch nor the posture is knowable, which is why the diagnostic lives at BIND and only fires where the answer is settled.

Why group is optional rather than required (#18378, ruling A′, 2026-09-16)

The 2026-09-08 ruling was made for the MULTI-TENANT shape. #18378 asked whether it binds a group — one legal group, one database, with group-wide visibility and cross-org workflow inherent to the posture (ADR-0105 D1, the multi-plant MES example is the ADR's own). It does not.

⚠️ The group row is not a fallback that guesses. It is the resolution order sys_automation_run has ALREADY been ruled to use: ObjectStoreSuspendedRunStore resolves a run's organization as organizationOf(<subject record>) ?? ctx.tenantId — subject first, the acting context as the fallback and never the primary (objectql/src/tenancy/platform-object-tenancy.ts, the sys_automation_run evidence line). Before this card the two halves disagreed under group: the history row was stamped from the record while the inbox and delivery rows followed an acting context that could not exist there, so they were refused. Filling the acting context from the record makes one run carry ONE organization's opinion about who it belonged to — which is the defect commit ecdfc9411 fixed, read from the other side.

⚠️ With ONE stated exception, so the sentence above is not read as a promise it cannot keep. The two halves ask different questions and are answered by different faces of the shared resolver: the history row is STAMPED (who is this row about — the platform stamp column wins there, by the cloud#1395 ruling), while the run's acting organization is a WALL reading (what is this row scoped by, which never consults that column). They give the same answer on every object where the two coincide — every ordinary object, because a stamp column is what makes them differ and exactly one shipped object has one (sys_api_key, deliberately unwalled, #8287; carried by PLATFORM_STAMP_ORGANIZATION_COLUMNS in @objectstack/metadata-core since the authorable tenancy.organizationField key was retired at protocol 18, #19054). Sweeping THAT object under group stamps the history row from its stamp column while the run itself acts as nothing and its inbox writes are refused. That is the correct pair of answers rather than a residue of the old disagreement — a row nothing walls has no organization for a run to act as, however clearly it says who it is about — but it is a divergence, and it is recorded here rather than smoothed over.

⛔ A record-less run under group that declared nothing still resolves NOTHING, and takes the existing walled-posture refusal at the write (resolveSystemWriteOrganization) — loud, by name, carrying the remedy. The card's original option A would have fallen back to the bootstrap organization (slug='default'); that arm was rejected on measurement. Under a wall AuthPlugin skips its own default-organization bootstrap and the enterprise organizations runtime mints one ADMIN-KEYED, so a group install with no resolvable platform admin holds no such organization at all; where one does exist it is whichever organization the platform owner registered under — plausibly one plant of many, not the group's head office. Landing a group-wide cron's notifications in one arbitrary plant's inbox is the wrong-owner failure the paragraph above forbids, not a lesser version of it.

Where it lives, and why there

On the flow's START node config, beside the cadence it scopes:

config: {
  schedule: { type: 'cron', expression: '0 8 * * *' },
  organization: 'org_msokm9oaz0cal87q',
}

The start node is where every other trigger-binding fact already lives — FlowSchema refuses a top-level schedule in as many words ("a schedule flow declares its cron/interval as config.schedule on the START node, not at the flow top level"), and resolveTriggerBinding hands the whole start config to the trigger. Putting the organization at the flow top level would split one binding across two layers; putting it inside the schedule descriptor would make it invisible to the time-relative sweep, which carries its cadence in the same slot but binds through a different descriptor. One key, one layer, both time triggers.

Why ScheduleOrganization… and not FlowActingOrganization…

The key governs both trigger kinds, and FlowTriggerKind lists time_relative and schedule as two of its four members — so the name looks inaccurate for half its subjects. It is not, and the deciding reading is the AUTHORABLE surface rather than the derived kind: FlowSchema.type is z.enum(['autolaunched', 'record_change', 'schedule', 'screen', 'api']) and has no time_relative member at all. A time-relative sweep is authored as type: 'schedule' with a timeRelative descriptor on its start node — the docs say so in as many words ("a schedule flow whose start node declares a timeRelative descriptor"), and TimeRelativeTriggerSchema's own opening line says the trigger "sweeps an object on a schedule". FlowTriggerKind splits the two because the ENGINE routes them to different triggers; its precedence note distinguishes a sweep from "a plain schedule flow", which is a split inside the schedule family, not out of it.

⇒ Every flow this key applies to declares type: 'schedule'. The name is accurate for both subjects, and a minor freezes it, so this is recorded rather than left to be re-litigated.

Source: packages/spec/src/automation/schedule-organization.zod.ts

TypeScript Usage

import { ScheduleOrganizationSchema } from '@objectstack/spec/automation';
import type { ScheduleOrganization } from '@objectstack/spec/automation';

// Validate data
const result = ScheduleOrganizationSchema.parse(data);

ScheduleOrganization

Organization id (sys_organization.id) this scheduled/time-relative flow runs as. A time-triggered run has no session to inherit a tenant from. Required under the isolated tenancy posture: a flow that declares none is not armed. Optional under group, where an undeclared run acts as the organization of the record it swept. Not required under single, where the run carries no organization.

Type: string


On this page