feat(triggers): declarative time-relative trigger (#1874)#3230
Merged
Conversation
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
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
Contributor
📓 Docs Drift CheckThis PR changes 5 package(s): 110 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:
|
…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
marked this pull request as ready for review
July 18, 2026 16:14
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).
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem (#1874)
Time-relative business rules ("alert 60 days before a contract's
end_date") could only be expressed as arecord_changeflow gated on a date-equality condition likeend_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 (contractsrenewal_alertT-60/30/7, hrdocument_expiring_soon, procurementpo_overdue, …).What this adds
A flow's start node can now declare a
timeRelativedescriptor instead:The new
time_relativetrigger (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-nodeconditiongate 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.withinDays: NdateField ∈ [today, today + N](upcoming).N < 0= overdue lookback.offsetDays: [a, b, …]today + a,today + b, …).Changes by package
@objectstack/spec— newTimeRelativeTriggerSchema(@objectstack/spec/automation), the Zod source of truth. Exactly one ofwithinDays|offsetDaysis required;object/dateFieldare snake_case (contract-first).@objectstack/service-automation—resolveTriggerBindingroutes a start node carryingconfig.timeRelativeto thetime_relativetrigger, ahead of the plainscheduletrigger (whose behavior is unchanged) since such a flow also carries aschedulecadence. Binding-audit hint mentions the new type.@objectstack/trigger-schedule—TimeRelativeTrigger+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 atmaxRecords/tick, and isolates per-record failures so one bad row never aborts the sweep.@objectstack/lint—os validategains readiness checks for the new descriptor (unknown swept object; ambiguous draft status).@objectstack/cli—serve.tsarmsTimeRelativeTriggerPluginin thetriggersgroup.Design notes
ScheduleTriggerPluginkeeps its exact dependency surface (job service only);TimeRelativeTriggerPluginadds the ObjectQL dependency it needs for the sweep query. Both ship in@objectstack/trigger-schedule(the affected package named in the issue).Flow.type. Time-relative flows staytype: 'schedule'; the trigger is selected by the presence ofconfig.timeRelative, so the flow-trigger conformance ledger (keyed on theFlow.typeenum) is unaffected.{today}are only resolved upstream, never byIDataEngine.find), mirroring the platform's ownLifecycleServiceretention sweep.Testing
TimeRelativeTriggerSchema— validation (mutual exclusivity, snake_case, bounds). ✅TimeRelativeTrigger— window math,wherebuilding, per-record fan-out, offset dedup,maxRecordscap, per-record + query error isolation, schedule default, unknown-object warning,stop()/idempotency, plugin wiring. ✅time_relativebinding resolution + precedence over the schedule trigger. ✅@objectstack/trigger-schedule(42),service-automationengine (121),specautomation (316),lint(11) suites pass; the full@objectstack/clibuild closure (54 packages) typechecks green. Changeset included.Closes #1874
🤖 Generated with Claude Code
Generated by Claude Code