Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions packages/all/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,10 @@
"clean": "rm -rf .objectstack/marketplace-packages dist"
},
"dependencies": {
"@objectstack/account": "^9.5.1",
"@objectstack/cli": "^9.5.1",
"@objectstack/driver-sqlite-wasm": "^9.5.1",
"@objectstack/runtime": "^9.5.1",
"@objectstack/account": "^9.7.0",
"@objectstack/cli": "^9.7.0",
"@objectstack/driver-sqlite-wasm": "^9.7.0",
"@objectstack/runtime": "^9.7.0",
"sql.js": "^1.14.1"
},
"optionalDependencies": {
Expand Down
22 changes: 11 additions & 11 deletions packages/compliance/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,17 +18,17 @@
"test": "objectstack build"
},
"dependencies": {
"@objectstack/account": "^9.5.1",
"@objectstack/cli": "^9.5.1",
"@objectstack/driver-memory": "^9.5.1",
"@objectstack/driver-sql": "^9.5.1",
"@objectstack/driver-sqlite-wasm": "^9.5.1",
"@objectstack/metadata": "^9.5.1",
"@objectstack/objectql": "^9.5.1",
"@objectstack/runtime": "^9.5.1",
"@objectstack/service-analytics": "^9.5.1",
"@objectstack/service-automation": "^9.5.1",
"@objectstack/spec": "^9.5.1",
"@objectstack/account": "^9.7.0",
"@objectstack/cli": "^9.7.0",
"@objectstack/driver-memory": "^9.7.0",
"@objectstack/driver-sql": "^9.7.0",
"@objectstack/driver-sqlite-wasm": "^9.7.0",
"@objectstack/metadata": "^9.7.0",
"@objectstack/objectql": "^9.7.0",
"@objectstack/runtime": "^9.7.0",
"@objectstack/service-analytics": "^9.7.0",
"@objectstack/service-automation": "^9.7.0",
"@objectstack/spec": "^9.7.0",
"sql.js": "^1.14.1"
},
"optionalDependencies": {
Expand Down
31 changes: 21 additions & 10 deletions packages/compliance/src/flows/evidence_auto_expire.flow.ts
Original file line number Diff line number Diff line change
@@ -1,38 +1,49 @@
// Copyright (c) 2026 ObjectStack contributors. Apache-2.0 license.

import type * as Automation from '@objectstack/spec/automation';
import { cel } from '@objectstack/spec';
type Flow = Automation.Flow;

/**
* Evidence Auto-Expire — flips approved evidence to "expired" the day
* after its expires_on date. Daily scheduler re-evaluates the criteria.
* Evidence Auto-Expire — flips approved evidence to `expired` once its
* `expires_on` date has passed.
*
* Why a SCHEDULED flow, not record-change (#1874): expiry is time-relative.
* A record-change trigger only fires on row mutation, so untouched evidence
* would never expire. A daily schedule re-evaluates against `today()`.
*
* Idempotency is the status transition: the bulk update only matches
* `status == 'approved'`, and expiring flips it to `expired`. No guard field.
*/
export const EvidenceAutoExpireFlow: Flow = {
name: 'compliance_evidence_auto_expire',
label: 'Auto-Expire Stale Evidence',
description: 'Flips approved evidence to expired status once expires_on has passed.',
type: 'record_change',
type: 'schedule',

variables: [{ name: 'evidenceId', type: 'text', isInput: true, isOutput: false }],
variables: [],

nodes: [
{
id: 'start',
type: 'start',
label: 'Start',
label: 'Start (Scheduled)',
config: {
objectName: 'compliance_evidence',
triggerType: 'record-after-update',
condition: 'status == "approved" && expires_on != null && expires_on == daysAgo(0)',
schedule: 'cron:0 1 * * *', // Daily at 1am
},
},
{
id: 'mark_expired',
type: 'update_record',
label: 'Mark Expired',
label: 'Expire Past-Due Evidence',
config: {
objectName: 'compliance_evidence',
filter: { id: '{record.id}' },
// Bulk update: every approved evidence row whose expiry has passed.
// The status flip is the idempotency guard.
filter: {
status: 'approved',
expires_on: { $lt: cel`today()` },
},
fields: { status: 'expired' },
},
},
Expand Down
74 changes: 51 additions & 23 deletions packages/compliance/src/flows/evidence_expiring.flow.ts
Original file line number Diff line number Diff line change
@@ -1,60 +1,88 @@
// Copyright (c) 2026 ObjectStack contributors. Apache-2.0 license.

import type * as Automation from '@objectstack/spec/automation';
import { cel } from '@objectstack/spec';
type Flow = Automation.Flow;

/**
* Evidence Expiring — alerts at T-30 and T-7 days before evidence
* expires_on, so collectors have time to re-pull. Same equality pattern
* as the contracts renewal alert.
* Evidence Expiring — alerts the collector at T-30 and T-7 days before an
* approved evidence record's `expires_on`, so they have time to re-pull.
*
* Why a SCHEDULED flow, not record-change (#1874): the alert days are time-
* relative. A record-change trigger only fires on row mutation, so it would
* almost never fire on the right day. The daily schedule selects the records
* landing on an alert day instead.
*
* Each tier is a one-day window `[daysFromNow(N), daysFromNow(N+1))` rather than
* an exact `== daysFromNow(N)`: `expires_on` carries a time component, so two
* independently-computed timestamps never compare equal, whereas the abutting
* 24h windows tile the timeline so a record falls in exactly one — fires once
* per tier, idempotent by construction (no guard field).
*/
export const EvidenceExpiringFlow: Flow = {
name: 'compliance_evidence_expiring',
label: 'Alert When Evidence Expiring',
description: 'Notifies evidence collector at T-30 and T-7 days before expires_on.',
type: 'record_change',
description:
'Daily scheduled job: notifies the evidence collector at T-30 and T-7 days before expires_on.',
type: 'schedule',

variables: [{ name: 'evidenceId', type: 'text', isInput: true, isOutput: false }],
variables: [],

nodes: [
{
id: 'start',
type: 'start',
label: 'Start',
label: 'Start (Scheduled)',
config: {
objectName: 'compliance_evidence',
triggerType: 'record-after-update',
condition:
'status == "approved" && expires_on != null && (expires_on == daysFromNow(30) || expires_on == daysFromNow(7))',
schedule: 'cron:0 9 * * *', // Daily at 9am
},
},
{
id: 'get_evidence',
id: 'query_expiring',
type: 'get_record',
label: 'Load Evidence',
label: 'Find Evidence Hitting an Alert Day',
config: {
objectName: 'compliance_evidence',
filter: { id: '{record.id}' },
outputVariable: 'ev',
filter: {
status: 'approved',
$or: [
{ expires_on: { $gte: cel`daysFromNow(7)`, $lt: cel`daysFromNow(8)` } },
{ expires_on: { $gte: cel`daysFromNow(30)`, $lt: cel`daysFromNow(31)` } },
],
},
limit: 500,
outputVariable: 'expiringEvidence',
},
},
{
id: 'foreach_evidence',
type: 'loop',
label: 'For Each Expiring Evidence',
config: {
collection: '{expiringEvidence.records}',
iteratorVar: 'evidence',
},
},
{
id: 'notify',
type: 'notify',
label: 'Notify Collector',
config: {
recipients: ['{ev.collected_by}'],
title: 'Evidence expiring soon: {ev.title}',
body: 'Evidence "{ev.title}" expires on {ev.expires_on}. Collect a fresh copy and resubmit.',
actionUrl: '/objects/compliance_evidence/{ev.id}',
recipients: ['{evidence.collected_by}'],
title: 'Evidence expiring soon: {evidence.title}',
body: 'Evidence "{evidence.title}" expires on {evidence.expires_on}. Collect a fresh copy and resubmit.',
actionUrl: '/objects/compliance_evidence/{evidence.id}',
},
},
{ id: 'end', type: 'end', label: 'End' },
{ id: 'end_loop', type: 'end', label: 'End Loop Iteration' },
{ id: 'end', type: 'end', label: 'End Flow' },
],

edges: [
{ id: 'e1', source: 'start', target: 'get_evidence', type: 'default' },
{ id: 'e2', source: 'get_evidence', target: 'notify', type: 'default' },
{ id: 'e3', source: 'notify', target: 'end', type: 'default' },
{ id: 'e1', source: 'start', target: 'query_expiring', type: 'default' },
{ id: 'e2', source: 'query_expiring', target: 'foreach_evidence', type: 'default' },
{ id: 'e3', source: 'foreach_evidence', target: 'notify', type: 'default' },
{ id: 'e4', source: 'notify', target: 'end_loop', type: 'default' },
{ id: 'e5', source: 'end_loop', target: 'end', type: 'default' },
],
};
22 changes: 11 additions & 11 deletions packages/content/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,17 +18,17 @@
"test": "objectstack build"
},
"dependencies": {
"@objectstack/account": "^9.5.1",
"@objectstack/cli": "^9.5.1",
"@objectstack/driver-memory": "^9.5.1",
"@objectstack/driver-sql": "^9.5.1",
"@objectstack/driver-sqlite-wasm": "^9.5.1",
"@objectstack/metadata": "^9.5.1",
"@objectstack/objectql": "^9.5.1",
"@objectstack/runtime": "^9.5.1",
"@objectstack/service-analytics": "^9.5.1",
"@objectstack/service-automation": "^9.5.1",
"@objectstack/spec": "^9.5.1",
"@objectstack/account": "^9.7.0",
"@objectstack/cli": "^9.7.0",
"@objectstack/driver-memory": "^9.7.0",
"@objectstack/driver-sql": "^9.7.0",
"@objectstack/driver-sqlite-wasm": "^9.7.0",
"@objectstack/metadata": "^9.7.0",
"@objectstack/objectql": "^9.7.0",
"@objectstack/runtime": "^9.7.0",
"@objectstack/service-analytics": "^9.7.0",
"@objectstack/service-automation": "^9.7.0",
"@objectstack/spec": "^9.7.0",
"sql.js": "^1.14.1"
},
"optionalDependencies": {
Expand Down
7 changes: 4 additions & 3 deletions packages/content/src/actions/record_metric_snapshot.action.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,10 @@

/**
* record_metric_snapshot — manual action on a `content_publication`.
* Inserts a `content_metric` row for the supplied period; the
* `publication_rollup` flow then refreshes denormalised totals on the
* publication and parent piece.
* Inserts a `content_metric` row for the supplied period. The denormalised
* totals on the publication and parent piece are native `Field.summary`
* roll-ups (#1870) — the engine recomputes them automatically when this metric
* row is inserted; no follow-up flow/action step is needed.
*
* Reflects how teams actually measure: copy/paste from GA / Mixpanel /
* Stripe weekly, not a real-time stream.
Expand Down
7 changes: 5 additions & 2 deletions packages/content/src/flows/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,16 @@
import { SignalToTopicPromotionFlow } from './signal_to_topic_promotion.flow';
import { CtaCreationDefaultFlow } from './cta_creation_default.flow';
import { PublishApprovalFlow } from './publish_approval.flow';
import { PublicationRollupFlow } from './publication_rollup.flow';
import { StampLifecycleTimestampsFlow } from './stamp_lifecycle_timestamps.flow';

// NOTE: publication → piece metric totals are now native `Field.summary`
// roll-ups on content_publication / content_piece (#1870), recomputed by the
// engine on child change. The old `publication_rollup` flow is removed — its
// script-node "aggregations" never ran (the automation engine has no aggregate
// node), so the totals were silently never updated.
export const allFlows = [
SignalToTopicPromotionFlow,
CtaCreationDefaultFlow,
PublishApprovalFlow,
PublicationRollupFlow,
StampLifecycleTimestampsFlow,
];
Loading
Loading