Skip to content

feat(triggers): declarative time-relative trigger (#1874)#3230

Merged
os-zhuang merged 3 commits into
mainfrom
claude/time-relative-trigger-ikme70
Jul 18, 2026
Merged

feat(triggers): declarative time-relative trigger (#1874)#3230
os-zhuang merged 3 commits into
mainfrom
claude/time-relative-trigger-ikme70

Conversation

@os-zhuang

Copy link
Copy Markdown
Contributor

Problem (#1874)

Time-relative business rules ("alert 60 days before a contract's end_date") could only be expressed as a record_change flow gated on a date-equality condition like end_date == daysFromNow(60). That predicate is evaluated only when the record happens to change, so it fires only if a record is edited on exactly the threshold day — i.e. almost never, unattended. The robust alternative was a hand-written cron + range query that every author re-implemented (contracts renewal_alert T-60/30/7, hr document_expiring_soon, procurement po_overdue, …).

What this adds

A flow's start node can now declare a timeRelative descriptor instead:

config: {
  timeRelative: {
    object: 'contracts',
    dateField: 'end_date',
    offsetDays: [60, 30, 7],       // T-minus reminders — fires on each threshold day
    // — or — withinDays: 30       // "expiring soon" range; negative = overdue lookback
    filter: { status: 'active' },  // optional, ANDed with the date window
    maxRecords: 1000,              // optional per-sweep cap (default 1000)
  },
  schedule: { type: 'cron', expression: '0 8 * * *' }, // optional; defaults to daily 08:00 UTC
  condition: '...',                // optional per-record start-condition gate
}

The new time_relative trigger (TimeRelativeTriggerPlugin, shipped in @objectstack/trigger-schedule) sweeps the object on that schedule and launches the flow once per matching record, with the record on the automation context — so the start-node condition gate and {record.<field>} interpolation work exactly as for a record-change flow. Because the window is evaluated every day, a threshold is never missed regardless of when the record last changed.

Mode Semantics (day-granular, UTC, always includes today)
withinDays: N dateField ∈ [today, today + N] (upcoming). N < 0 = overdue lookback.
offsetDays: [a, b, …] one single-day match per offset (today + a, today + b, …).

Changes by package

  • @objectstack/spec — new TimeRelativeTriggerSchema (@objectstack/spec/automation), the Zod source of truth. Exactly one of withinDays | offsetDays is required; object/dateField are snake_case (contract-first).
  • @objectstack/service-automationresolveTriggerBinding routes a start node carrying config.timeRelative to the time_relative trigger, ahead of the plain schedule trigger (whose behavior is unchanged) since such a flow also carries a schedule cadence. Binding-audit hint mentions the new type.
  • @objectstack/trigger-scheduleTimeRelativeTrigger + TimeRelativeTriggerPlugin. Composes the schedule trigger's job service (sweep cadence) with the ObjectQL engine (date-window query, the canonical { dateField: { $gte, $lte } } map form with concrete ISO bounds). The discovery query runs as a system operation (RLS-bypassing — a background sweep sees all rows), is capped at maxRecords/tick, and isolates per-record failures so one bad row never aborts the sweep.
  • @objectstack/lintos validate gains readiness checks for the new descriptor (unknown swept object; ambiguous draft status).
  • @objectstack/cliserve.ts arms TimeRelativeTriggerPlugin in the triggers group.

Design notes

  • Separate plugin, same package. The plain ScheduleTriggerPlugin keeps its exact dependency surface (job service only); TimeRelativeTriggerPlugin adds the ObjectQL dependency it needs for the sweep query. Both ship in @objectstack/trigger-schedule (the affected package named in the issue).
  • No new Flow.type. Time-relative flows stay type: 'schedule'; the trigger is selected by the presence of config.timeRelative, so the flow-trigger conformance ledger (keyed on the Flow.type enum) is unaffected.
  • Not a raw-engine date-macro. The sweep computes concrete ISO window bounds itself (date macros like {today} are only resolved upstream, never by IDataEngine.find), mirroring the platform's own LifecycleService retention sweep.

Testing

  • TimeRelativeTriggerSchema — validation (mutual exclusivity, snake_case, bounds). ✅
  • TimeRelativeTrigger — window math, where building, per-record fan-out, offset dedup, maxRecords cap, per-record + query error isolation, schedule default, unknown-object warning, stop()/idempotency, plugin wiring. ✅
  • Engine — time_relative binding resolution + precedence over the schedule trigger. ✅
  • Lint — time-relative auto-triggered + unknown-object checks. ✅

@objectstack/trigger-schedule (42), service-automation engine (121), spec automation (316), lint (11) suites pass; the full @objectstack/cli build closure (54 packages) typechecks green. Changeset included.

Closes #1874

🤖 Generated with Claude Code


Generated by Claude Code

Time-relative business rules ("alert 60 days before a contract's end_date")
could only be expressed as a record_change flow gated on a date-equality
condition like `end_date == daysFromNow(60)`. That predicate is evaluated only
when the record happens to change, so it fires only if a record is edited on
exactly the threshold day — i.e. almost never, unattended. The robust
alternative was a hand-written cron + range query that every author
re-implemented (contracts renewal_alert, hr document_expiring_soon,
procurement po_overdue, ...).

A flow's start node can now declare a `timeRelative` descriptor:

  config: {
    timeRelative: {
      object: 'contracts',
      dateField: 'end_date',
      offsetDays: [60, 30, 7],       // T-minus reminders — fires on each threshold day
      // — or — withinDays: 30       // "expiring soon" range; negative = overdue lookback
      filter: { status: 'active' },  // optional, ANDed with the date window
    },
    schedule: { type: 'cron', expression: '0 8 * * *' }, // optional; default daily 08:00 UTC
  }

The new time_relative trigger (TimeRelativeTriggerPlugin, shipped in
@objectstack/trigger-schedule) sweeps the object on that schedule and launches
the flow once per matching record, with the record on the automation context —
so the start-node condition gate and {record.<field>} interpolation work exactly
as for a record-change flow. Because the window is evaluated every day, a
threshold is never missed regardless of when the record last changed.

- spec: new TimeRelativeTriggerSchema (@objectstack/spec/automation), the Zod
  source of truth; exactly one of withinDays | offsetDays is required.
- engine: resolveTriggerBinding routes a start node carrying config.timeRelative
  to the time_relative trigger, ahead of the plain schedule trigger (whose
  behavior is unchanged) since such a flow also carries a schedule cadence.
- trigger: composes the schedule trigger's job service (sweep cadence) with the
  ObjectQL engine (date-window query). The discovery query runs as a system
  operation (RLS-bypassing), is capped at maxRecords/tick (default 1000), and
  isolates per-record failures so one bad row never aborts the sweep.
- lint: os validate gains readiness checks for the new descriptor (unknown swept
  object, ambiguous draft status).
- cli: serve.ts arms the plugin in the triggers group.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AHzW68suiFuu6GdJyea8U4
@vercel

vercel Bot commented Jul 18, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
spec Canceled Canceled Jul 18, 2026 4:02pm

Request Review

@github-actions github-actions Bot added documentation Improvements or additions to documentation tests tooling size/xl labels Jul 18, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 5 package(s): @objectstack/cli, @objectstack/lint, packages/services, @objectstack/spec, packages/triggers.

110 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:

  • content/docs/ai/agents.mdx (via @objectstack/spec)
  • content/docs/ai/skills-reference.mdx (via packages/cli, @objectstack/spec)
  • content/docs/ai/skills.mdx (via @objectstack/spec)
  • content/docs/api/client-sdk.mdx (via @objectstack/cli, @objectstack/spec)
  • content/docs/api/data-flow.mdx (via @objectstack/cli)
  • content/docs/api/environment-routing.mdx (via @objectstack/cli, @objectstack/spec)
  • content/docs/api/error-catalog.mdx (via @objectstack/cli, @objectstack/spec)
  • content/docs/api/error-handling-client.mdx (via @objectstack/spec)
  • content/docs/api/error-handling-server.mdx (via @objectstack/spec)
  • content/docs/api/index.mdx (via @objectstack/spec)
  • content/docs/automation/approvals.mdx (via packages/spec)
  • content/docs/automation/flows.mdx (via @objectstack/spec)
  • content/docs/automation/hook-bodies.mdx (via packages/cli, packages/spec)
  • content/docs/automation/hooks.mdx (via @objectstack/spec)
  • content/docs/automation/index.mdx (via @objectstack/spec)
  • content/docs/automation/webhooks.mdx (via packages/services, @objectstack/spec)
  • content/docs/automation/workflows.mdx (via @objectstack/spec)
  • content/docs/concepts/architecture.mdx (via @objectstack/spec)
  • content/docs/concepts/design-principles.mdx (via packages/spec)
  • content/docs/concepts/index.mdx (via @objectstack/spec)
  • content/docs/concepts/metadata-driven.mdx (via @objectstack/spec)
  • content/docs/concepts/metadata-lifecycle.mdx (via packages/spec)
  • content/docs/concepts/north-star.mdx (via packages/spec)
  • content/docs/data-modeling/analytics.mdx (via @objectstack/spec)
  • content/docs/data-modeling/drivers.mdx (via @objectstack/spec)
  • content/docs/data-modeling/external-datasources.mdx (via @objectstack/spec)
  • content/docs/data-modeling/field-types.mdx (via @objectstack/spec)
  • content/docs/data-modeling/fields.mdx (via @objectstack/spec)
  • content/docs/data-modeling/formulas.mdx (via @objectstack/spec)
  • content/docs/data-modeling/index.mdx (via @objectstack/spec)
  • content/docs/data-modeling/objects.mdx (via @objectstack/spec)
  • content/docs/data-modeling/queries.mdx (via @objectstack/spec)
  • content/docs/data-modeling/schema-design.mdx (via @objectstack/spec)
  • content/docs/data-modeling/seed-data.mdx (via @objectstack/spec)
  • content/docs/data-modeling/validation-rules.mdx (via @objectstack/spec)
  • content/docs/data-modeling/validation.mdx (via @objectstack/spec)
  • content/docs/deployment/backup-restore.mdx (via @objectstack/cli)
  • content/docs/deployment/self-hosting.mdx (via @objectstack/cli)
  • content/docs/deployment/troubleshooting.mdx (via @objectstack/spec)
  • content/docs/getting-started/build-with-claude-code.mdx (via @objectstack/spec)
  • content/docs/getting-started/cli.mdx (via @objectstack/cli, @objectstack/spec)
  • content/docs/getting-started/common-patterns.mdx (via @objectstack/spec)
  • content/docs/getting-started/examples.mdx (via @objectstack/spec)
  • content/docs/getting-started/quick-reference.mdx (via @objectstack/spec)
  • content/docs/getting-started/quick-start.mdx (via @objectstack/spec)
  • content/docs/getting-started/validating-metadata.mdx (via @objectstack/spec)
  • content/docs/getting-started/your-first-project.mdx (via @objectstack/cli, @objectstack/spec)
  • content/docs/kernel/cluster.mdx (via @objectstack/spec)
  • content/docs/kernel/contracts/auth-service.mdx (via packages/spec)
  • content/docs/kernel/contracts/cache-service.mdx (via packages/spec)
  • content/docs/kernel/contracts/data-engine.mdx (via @objectstack/spec)
  • content/docs/kernel/contracts/index.mdx (via @objectstack/spec)
  • content/docs/kernel/contracts/metadata-service.mdx (via packages/spec)
  • content/docs/kernel/contracts/storage-service.mdx (via packages/spec)
  • content/docs/kernel/index.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/audit-service.mdx (via packages/services)
  • content/docs/kernel/runtime-services/data-service.mdx (via packages/cli)
  • content/docs/kernel/runtime-services/email-service.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/index.mdx (via packages/cli, packages/services, packages/spec)
  • content/docs/kernel/runtime-services/queue-service.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/settings-service.mdx (via packages/services)
  • content/docs/kernel/runtime-services/sharing-service.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/sms-service.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/storage-service.mdx (via packages/spec)
  • content/docs/kernel/services-checklist.mdx (via @objectstack/spec)
  • content/docs/permissions/authentication.mdx (via @objectstack/cli)
  • content/docs/permissions/authorization.mdx (via @objectstack/lint, @objectstack/spec)
  • content/docs/permissions/permission-sets.mdx (via @objectstack/spec)
  • content/docs/permissions/permissions-matrix.mdx (via @objectstack/spec)
  • content/docs/permissions/positions.mdx (via @objectstack/spec)
  • content/docs/permissions/rls.mdx (via @objectstack/spec)
  • content/docs/permissions/sharing-rules.mdx (via @objectstack/spec)
  • content/docs/plugins/adding-a-metadata-type.mdx (via @objectstack/spec)
  • content/docs/plugins/development.mdx (via @objectstack/spec)
  • content/docs/plugins/index.mdx (via @objectstack/spec)
  • content/docs/plugins/packages.mdx (via @objectstack/cli, packages/services, @objectstack/spec)
  • content/docs/protocol/backward-compatibility.mdx (via @objectstack/spec)
  • content/docs/protocol/diagram.mdx (via packages/spec)
  • content/docs/protocol/kernel/config-resolution.mdx (via @objectstack/spec)
  • content/docs/protocol/kernel/i18n-standard.mdx (via packages/services, @objectstack/spec)
  • content/docs/protocol/kernel/lifecycle.mdx (via @objectstack/spec)
  • content/docs/protocol/kernel/plugin-spec.mdx (via @objectstack/cli, @objectstack/spec)
  • content/docs/protocol/kernel/realtime-protocol.mdx (via @objectstack/cli)
  • content/docs/protocol/kernel/runtime-capabilities.mdx (via @objectstack/spec)
  • content/docs/protocol/knowledge.mdx (via @objectstack/spec)
  • content/docs/protocol/objectql/index.mdx (via packages/spec)
  • content/docs/protocol/objectql/query-syntax.mdx (via @objectstack/spec)
  • content/docs/protocol/objectql/schema.mdx (via @objectstack/spec)
  • content/docs/protocol/objectql/security.mdx (via packages/spec)
  • content/docs/protocol/objectql/state-machine.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/actions.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/concept.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/index.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/layout-dsl.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/record-alert.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/widget-contract.mdx (via @objectstack/spec)
  • content/docs/releases/implementation-status.mdx (via @objectstack/cli, @objectstack/spec)
  • content/docs/releases/index.mdx (via @objectstack/spec)
  • content/docs/releases/v12.mdx (via @objectstack/spec)
  • content/docs/releases/v13.mdx (via @objectstack/spec)
  • content/docs/releases/v9.mdx (via @objectstack/spec)
  • content/docs/ui/actions.mdx (via @objectstack/spec)
  • content/docs/ui/create-vs-edit-form.mdx (via @objectstack/spec)
  • content/docs/ui/dashboards.mdx (via @objectstack/spec)
  • content/docs/ui/forms.mdx (via @objectstack/spec)
  • content/docs/ui/index.mdx (via @objectstack/spec)
  • content/docs/ui/public-data-collection.mdx (via @objectstack/spec)
  • content/docs/ui/setup-app.mdx (via @objectstack/spec)
  • content/docs/ui/translations.mdx (via @objectstack/spec)
  • content/docs/ui/views.mdx (via @objectstack/spec)

Advisory only. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs origin/main → pass the list as args.docs.

claude added 2 commits July 18, 2026 15:47
…gger (#1874)

Auto-generated from the new TimeRelativeTriggerSchema (packages/spec).
content/docs/references/ is generated by build-docs.ts and gated by the
`check:docs` CI job — this adds the missing automation/time-relative-trigger.mdx
and updates the automation index/meta.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AHzW68suiFuu6GdJyea8U4
…rts (#1874)

gen:api-surface — records the 5 new @objectstack/spec/automation exports
(TimeRelativeTriggerSchema, TimeRelativeTrigger, TimeRelativeTriggerInput,
TIME_RELATIVE_DEFAULT_CRON, TIME_RELATIVE_DEFAULT_MAX_RECORDS). Additive only:
0 breaking, 5 added. Gated by the check:api-surface CI job.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AHzW68suiFuu6GdJyea8U4
@os-zhuang
os-zhuang marked this pull request as ready for review July 18, 2026 16:14
@os-zhuang
os-zhuang merged commit a2795f6 into main Jul 18, 2026
17 checks passed
@os-zhuang
os-zhuang deleted the claude/time-relative-trigger-ikme70 branch July 18, 2026 16:15
os-zhuang added a commit that referenced this pull request Jul 18, 2026
Adds the declarative time-relative trigger to the two hand-written author-facing surfaces: skills/objectstack-automation/SKILL.md (a Time-relative triggers section, framed as the replacement for the date-equality-on-record-change anti-pattern) and content/docs/automation/flows.mdx (a Time-relative flow example). The auto-generated reference and package README landed with the feature (#3230).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/xl tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[P2] Provide a declarative time-relative trigger (avoid fragile date-equality on record-change)

2 participants