@aglyn/plugins-workflows 1.0.0-beta.143

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (75) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +52 -0
  3. package/package.json +53 -0
  4. package/src/index.d.ts +18 -0
  5. package/src/index.js +19 -0
  6. package/src/index.js.map +1 -0
  7. package/src/lib/components/automation-step-fields.component.d.ts +74 -0
  8. package/src/lib/components/automation-step-fields.component.js +1210 -0
  9. package/src/lib/components/automation-step-fields.component.js.map +1 -0
  10. package/src/lib/components/host-actions-card.component.d.ts +15 -0
  11. package/src/lib/components/host-actions-card.component.js +1229 -0
  12. package/src/lib/components/host-actions-card.component.js.map +1 -0
  13. package/src/lib/components/host-activity-card.component.d.ts +84 -0
  14. package/src/lib/components/host-activity-card.component.js +280 -0
  15. package/src/lib/components/host-activity-card.component.js.map +1 -0
  16. package/src/lib/components/host-run-history-card.component.d.ts +40 -0
  17. package/src/lib/components/host-run-history-card.component.js +284 -0
  18. package/src/lib/components/host-run-history-card.component.js.map +1 -0
  19. package/src/lib/components/host-webhooks-card.component.d.ts +15 -0
  20. package/src/lib/components/host-webhooks-card.component.js +421 -0
  21. package/src/lib/components/host-webhooks-card.component.js.map +1 -0
  22. package/src/lib/components/host-workflows-card.component.d.ts +18 -0
  23. package/src/lib/components/host-workflows-card.component.js +952 -0
  24. package/src/lib/components/host-workflows-card.component.js.map +1 -0
  25. package/src/lib/components/run-quota-line.component.d.ts +27 -0
  26. package/src/lib/components/run-quota-line.component.js +72 -0
  27. package/src/lib/components/run-quota-line.component.js.map +1 -0
  28. package/src/lib/components/use-automation-step-pickers.d.ts +27 -0
  29. package/src/lib/components/use-automation-step-pickers.js +205 -0
  30. package/src/lib/components/use-automation-step-pickers.js.map +1 -0
  31. package/src/lib/components/workflow-zones.d.ts +36 -0
  32. package/src/lib/components/workflow-zones.js +19 -0
  33. package/src/lib/components/workflow-zones.js.map +1 -0
  34. package/src/lib/components/workflows-console-page.d.ts +25 -0
  35. package/src/lib/components/workflows-console-page.js +112 -0
  36. package/src/lib/components/workflows-console-page.js.map +1 -0
  37. package/src/lib/components/workflows-console-sections.d.ts +34 -0
  38. package/src/lib/components/workflows-console-sections.js +50 -0
  39. package/src/lib/components/workflows-console-sections.js.map +1 -0
  40. package/src/lib/constants/bundle-common.d.ts +8 -0
  41. package/src/lib/constants/bundle-common.js +9 -0
  42. package/src/lib/constants/bundle-common.js.map +1 -0
  43. package/src/lib/declarations.server.d.ts +41 -0
  44. package/src/lib/declarations.server.js +54 -0
  45. package/src/lib/declarations.server.js.map +1 -0
  46. package/src/lib/engine/crm-action-steps.d.ts +129 -0
  47. package/src/lib/engine/crm-action-steps.js +432 -0
  48. package/src/lib/engine/crm-action-steps.js.map +1 -0
  49. package/src/lib/engine/flow-enrollments.d.ts +264 -0
  50. package/src/lib/engine/flow-enrollments.js +386 -0
  51. package/src/lib/engine/flow-enrollments.js.map +1 -0
  52. package/src/lib/engine/run-event-actions.d.ts +232 -0
  53. package/src/lib/engine/run-event-actions.js +1392 -0
  54. package/src/lib/engine/run-event-actions.js.map +1 -0
  55. package/src/lib/engine/run-event-automations.d.ts +28 -0
  56. package/src/lib/engine/run-event-automations.js +37 -0
  57. package/src/lib/engine/run-event-automations.js.map +1 -0
  58. package/src/lib/engine/run-event-workflows.d.ts +75 -0
  59. package/src/lib/engine/run-event-workflows.js +311 -0
  60. package/src/lib/engine/run-event-workflows.js.map +1 -0
  61. package/src/lib/engine/workflow-steps.d.ts +107 -0
  62. package/src/lib/engine/workflow-steps.js +124 -0
  63. package/src/lib/engine/workflow-steps.js.map +1 -0
  64. package/src/lib/plugin.d.ts +33 -0
  65. package/src/lib/plugin.js +94 -0
  66. package/src/lib/plugin.js.map +1 -0
  67. package/src/lib/server-automation-drafts.d.ts +90 -0
  68. package/src/lib/server-automation-drafts.js +429 -0
  69. package/src/lib/server-automation-drafts.js.map +1 -0
  70. package/src/lib/server-console.d.ts +27 -0
  71. package/src/lib/server-console.js +35 -0
  72. package/src/lib/server-console.js.map +1 -0
  73. package/src/lib/server.d.ts +29 -0
  74. package/src/lib/server.js +298 -0
  75. package/src/lib/server.js.map +1 -0
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/workflows/src/lib/engine/flow-enrollments.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * WHERE A PERSON WAITS, between one step of a flow and the next.\n *\n * An action used to be trigger → immediate steps, start to finish inside the\n * request that emitted the event. A `wait` step ends that: the run stops, and\n * something else has to pick it up minutes or weeks later. This module is the\n * something else's memory.\n *\n * ## The scheduling model, and why it survives a deploy\n *\n * There is no timer anywhere. A wait is a ROW — `resumeAtMs` on a document in\n * `hosts/{hostId}/flowEnrollments` — and the resume is a query for rows whose\n * time has come, run by the platform job beat that already exists. Nothing is\n * held in a process, so nothing is lost when the process ends: a deploy, a\n * restart, a cold start and a region failover all leave the row exactly where\n * it was, and the next beat finds it. The two worked examples in this repo\n * take the same shape for the same reason — `campaign-process-scheduled.ts`\n * claims `status: 'scheduled'` campaigns whose `sendAtMs` has passed, and the\n * abandoned-checkout scan re-reads any checkout it did not stamp.\n *\n * A beat that dies mid-resume is the case worth stating. The claim below flips\n * `waiting` → `running` in a transaction, so a second beat cannot pick up the\n * same enrollment; and a `running` row whose beat never came back is re-armed\n * by {@link FLOW_CLAIM_STALE_MS}, which is the difference between \"somebody is\n * working on this\" and \"somebody died holding this\".\n *\n * ## What it costs, which is not \"every enrollment on every beat\"\n *\n * The sweep reads DUE rows and only due rows: the query carries\n * `resumeAtMs <= now`, so a site with ten thousand people waiting three days\n * costs nothing on the beats before those three days are up. That is the whole\n * difference between this and the naive design, which re-reads every enrolled\n * person on every beat to ask whether their time has come.\n *\n * On top of that, {@link FLOW_RESUME_SCAN_BUDGET} bounds one beat's work and a\n * cursor resumes it — the shape `dynamic-list-materialize.ts` uses. The budget\n * matters even though the query is already narrow, because a due row that\n * cannot be acted on (a locked host) stays due: without paging past it, one\n * wall of locked rows at the head of the queue would starve every enrollment\n * behind it for as long as the lock lasted.\n *\n * ## ⛔ A BUDGET NEVER DROPS AN ENROLLMENT\n *\n * Running out of budget stops the sweep where it is. It never marks an\n * enrollment done, never advances its step, and never deletes it — the row\n * stays `waiting` and overdue, and the next beat starts again from the front\n * of the queue, which is where the oldest overdue work is. The same rule the\n * list materializer states: a bound on WORK is not a bound on PEOPLE.\n */\n\nimport { personKey } from '@aglyn/aglyn/server'\nimport { firebaseAdmin } from '@aglyn/tenant-data-admin'\nimport type { PluginJobHostGate } from '@aglyn/aglyn/server'\nimport type { WorkflowStep } from './workflow-steps'\n\n/**\n * `hosts/{hostId}/flowEnrollments/{actionId__personKey}` for an action, and\n * `{workflow-<workflowId>__personKey}` for a workflow.\n */\nexport const FLOW_ENROLLMENTS_SUBCOLLECTION = 'flowEnrollments'\n\n/**\n * Documents one sweep may READ across every site.\n *\n * A bound on work, not on membership — see the note above. Sized well past a\n * normal beat's due queue so the ordinary case never pages at all, and far\n * short of what one API invocation should spend.\n */\nexport const FLOW_RESUME_SCAN_BUDGET = 500\n\n/** Due enrollments read per page. */\nconst PAGE_SIZE = 50\n\n/**\n * How long a claim may go unfinished before another beat may take it.\n *\n * A `running` row is one a beat is part-way through. If that beat's process\n * ends — a deploy mid-resume, a function timeout, an unhandled throw that\n * escaped the isolation — nothing ever clears the flag, and without this the\n * enrollment would be stranded `running` for ever: not waiting, so no sweep\n * finds it; not done, so nobody is told.\n *\n * Ten minutes is far longer than a resume takes (a handful of Firestore reads\n * and at most one email) and far shorter than the shortest wait anybody\n * authors, so the only thing it can re-arm is a claim nobody is holding.\n *\n * The cost of getting it wrong is a step running twice, which is why the\n * window is generous rather than tight.\n */\nexport const FLOW_CLAIM_STALE_MS = 10 * 60_000\n\nexport type FlowEnrollmentStatus = 'waiting' | 'running'\n\n/**\n * Why an enrollment ended. Recorded on the run history, never on the\n * enrollment — the row is deleted when the flow ends, so the activity\n * collection is where \"what happened to this person\" lives.\n */\nexport type FlowEnrollmentEnding =\n /** Every remaining step ran. */\n | 'completed'\n /** An `exitFlow` step, or a guard that ended the run. */\n | 'exited'\n /** The action was deleted, disabled, or lost its entitlement. */\n | 'flow-stopped'\n\nexport interface FlowEnrollment {\n hostId: string\n /**\n * What is waiting: `workflow` for a workflow's run, absent for an action's\n * — which is every enrollment written before a workflow could wait, so an\n * absent field is read as an action for as long as those rows exist.\n */\n automation?: 'workflow'\n /** The automation's document id: the action's, or the workflow's. */\n actionId: string\n actionName: string\n status: FlowEnrollmentStatus\n /** When this flow continues. ALWAYS set — the sweep orders on it. */\n resumeAtMs: number\n /** Index into {@link steps} of the step that runs on resume. */\n nextStepIndex: number\n /**\n * THE FLOW AS IT WAS WHEN THIS PERSON ENTERED IT.\n *\n * Not a reference to the action's current steps, and this is the whole\n * answer to \"what happens to a flow somebody is already waiting inside\".\n * `nextStepIndex` is a position in a LIST, and an author who reorders,\n * inserts or deletes a step moves every position after it — so resuming a\n * three-day wait against the edited list would deliver whatever now happens\n * to sit at index 4. That is not a changed flow, it is a scrambled one, and\n * the person it happens to is the one who cannot see it.\n *\n * So an edit applies to everyone who enrolls AFTER it, and nobody who is\n * already inside. The trade is that a merchant who fixes a typo in step 3\n * does not fix it for the people mid-wait; the alternative trade is sending\n * them a step from a different flow, which is worse and silent.\n *\n * Bounded by `ACTION_MAX_STEPS` for an action, so the snapshot is ten\n * small objects, and by `WORKFLOW_MAX_STEPS` for a workflow, whose snapshot\n * holds its function calls beside its Actions steps.\n */\n steps: WorkflowStep[]\n /** The event a `waitForEvent` is watching for; absent for a plain wait. */\n awaitingEvent?: string | null\n /** `sha256` of the person's address — the wake lookup key. */\n personKey: string\n email: string\n /** The trigger payload, carried forward so later steps see the same scope. */\n payload: Record<string, unknown>\n /** The event that started the flow, for the run-history line. */\n event: string\n enrolledAtMs: number\n updatedAtMs: number\n /** How many times this enrollment has been picked up. */\n resumes: number\n /** Set while `running`; how {@link FLOW_CLAIM_STALE_MS} is measured. */\n claimedAtMs?: number\n}\n\n/**\n * ONE LIVE ENROLLMENT PER PERSON PER FLOW, by construction.\n *\n * The document id is derived rather than generated, so a second concurrent\n * enrollment is not a race to detect — it is a write to a document that\n * already exists. A shopper who abandons three carts in an hour gets one\n * recovery sequence, not three overlapping ones, and the guarantee holds\n * across processes because it is Firestore's and not a lock of ours.\n *\n * DELIBERATE, and reversible: a flow that genuinely wants concurrent\n * enrollments would key on something narrower than the person (the cart, the\n * order), which is a change to this function and to nothing else.\n */\nexport function flowEnrollmentId(\n actionId: string,\n key: string,\n automation?: 'workflow',\n): string {\n // A workflow's id is kept apart from an action's, so the two kinds can\n // never share a row whatever ids their collections hand out.\n return automation === 'workflow'\n ? `workflow-${actionId}__${key}`\n : `${actionId}__${key}`\n}\n\nfunction enrollmentsRef(\n hostId: string,\n firestore?: any,\n): FirebaseFirestore.CollectionReference {\n return (firestore ?? firebaseAdmin.app().firestore())\n .collection('hosts')\n .doc(hostId)\n .collection(FLOW_ENROLLMENTS_SUBCOLLECTION)\n}\n\nexport interface EnrollInFlowOptions {\n hostId: string\n /** `workflow` for a workflow's run; absent for an action's. */\n automation?: 'workflow'\n /** The automation's document id. */\n actionId: string\n action: { name?: string; steps?: readonly WorkflowStep[] }\n /** The address the flow is about. A flow with no person cannot wait. */\n email: string\n event: string\n payload: Record<string, unknown>\n /** Index of the step that runs when the wait ends. */\n nextStepIndex: number\n resumeAtMs: number\n awaitingEvent?: string | null\n nowMs?: number\n firestore?: any\n}\n\nexport type EnrollInFlowResult =\n | { enrolled: true; id: string }\n | { enrolled: false; reason: 'no-person' | 'already-waiting' }\n\n/**\n * Suspends a run, durably.\n *\n * Refuses rather than improvises when there is nobody to wait for. A flow\n * that waits is a flow that continues later for a PERSON — it is how the\n * enrollment is keyed, how a `waitForEvent` is woken, and who the next step's\n * email is addressed to. An anonymous wait would have no dedupe key, so a\n * page-view trigger on a busy site would mint an enrollment per visit; and\n * nothing downstream could use it. Saying so is better than silently\n * enrolling nobody.\n */\nexport async function enrollInFlow(\n options: EnrollInFlowOptions,\n): Promise<EnrollInFlowResult> {\n const key = personKey(options.email)\n if (!key) return { enrolled: false, reason: 'no-person' }\n const nowMs = options.nowMs ?? Date.now()\n const ref = enrollmentsRef(options.hostId, options.firestore).doc(\n flowEnrollmentId(options.actionId, key, options.automation),\n )\n const firestore = options.firestore ?? firebaseAdmin.app().firestore()\n return await firestore.runTransaction(\n async (transaction: FirebaseFirestore.Transaction) => {\n const existing = await transaction.get(ref)\n /*\n * A row that is `waiting` or freshly `running` belongs to a live\n * enrollment and this one is a duplicate. A STALE `running` row is a\n * claim nobody is holding (see FLOW_CLAIM_STALE_MS) and a completed\n * flow deletes its row, so anything else is free to be overwritten —\n * which is what lets the same person go through the same welcome series\n * again next year.\n */\n if (existing.exists) {\n const status = existing.get('status')\n const claimedAtMs = Number(existing.get('claimedAtMs') ?? 0)\n const live =\n status === 'waiting' ||\n (status === 'running' && nowMs - claimedAtMs < FLOW_CLAIM_STALE_MS)\n if (live) {\n return { enrolled: false, reason: 'already-waiting' as const }\n }\n }\n const enrollment: FlowEnrollment = {\n hostId: options.hostId,\n ...(options.automation === 'workflow'\n ? { automation: 'workflow' as const }\n : {}),\n actionId: options.actionId,\n actionName: String(options.action.name ?? ''),\n status: 'waiting',\n resumeAtMs: options.resumeAtMs,\n nextStepIndex: options.nextStepIndex,\n steps: [...(options.action.steps ?? [])],\n personKey: key,\n email: String(options.email).trim().toLowerCase(),\n payload: options.payload ?? {},\n event: options.event,\n enrolledAtMs: nowMs,\n updatedAtMs: nowMs,\n resumes: 0,\n ...(options.awaitingEvent\n ? { awaitingEvent: options.awaitingEvent }\n : { awaitingEvent: null }),\n }\n // `set`, not `create`: the transaction above has already decided this id\n // is free, and a completed flow's deleted row may be re-enrolled.\n transaction.set(ref, enrollment)\n return { enrolled: true as const, id: ref.id }\n },\n )\n}\n\n/**\n * Takes an enrollment out of the queue for this beat, or answers null.\n *\n * The claim and the read are one transaction, so two beats — an overlapping\n * schedule, a retried invocation, two regions — cannot both resume the same\n * person. `campaign-process-scheduled.ts` claims a due campaign exactly this\n * way and for exactly this reason.\n */\nexport async function claimFlowEnrollment(\n ref: FirebaseFirestore.DocumentReference,\n options?: { nowMs?: number; firestore?: any },\n): Promise<FlowEnrollment | null> {\n const nowMs = options?.nowMs ?? Date.now()\n const firestore =\n options?.firestore ?? ref.firestore ?? firebaseAdmin.app().firestore()\n return await firestore.runTransaction(\n async (transaction: FirebaseFirestore.Transaction) => {\n const fresh = await transaction.get(ref)\n if (!fresh.exists) return null\n const status = fresh.get('status')\n const claimedAtMs = Number(fresh.get('claimedAtMs') ?? 0)\n const claimable =\n status === 'waiting' ||\n (status === 'running' && nowMs - claimedAtMs >= FLOW_CLAIM_STALE_MS)\n if (!claimable) return null\n transaction.update(ref, {\n status: 'running',\n claimedAtMs: nowMs,\n updatedAtMs: nowMs,\n })\n return { ...(fresh.data() as FlowEnrollment), status: 'running' }\n },\n )\n}\n\n/**\n * Puts a claimed enrollment back without having run it.\n *\n * SKIPPED, NOT DROPPED. Used when the resume cannot proceed for a reason a\n * later beat may pass — the platform's hourly send ceiling had no room, the\n * recipient's own frequency window is full — so the step is attempted again\n * rather than lost. `retryAtMs` pushes the row down the queue so it does not\n * spin on the same refusal every minute.\n */\nexport async function deferFlowEnrollment(\n ref: FirebaseFirestore.DocumentReference,\n retryAtMs: number,\n nowMs = Date.now(),\n): Promise<void> {\n await ref\n .set(\n {\n status: 'waiting',\n resumeAtMs: retryAtMs,\n claimedAtMs: null,\n updatedAtMs: nowMs,\n },\n { merge: true },\n )\n .catch(() => undefined)\n}\n\n/** Advances a claimed enrollment to the next wait, keeping it in the queue. */\nexport async function advanceFlowEnrollment(\n ref: FirebaseFirestore.DocumentReference,\n update: {\n nextStepIndex: number\n resumeAtMs: number\n awaitingEvent?: string | null\n payload?: Record<string, unknown>\n },\n nowMs = Date.now(),\n): Promise<void> {\n await ref\n .set(\n {\n status: 'waiting',\n claimedAtMs: null,\n updatedAtMs: nowMs,\n resumes: firebaseAdmin.firestore.FieldValue.increment(1),\n nextStepIndex: update.nextStepIndex,\n resumeAtMs: update.resumeAtMs,\n awaitingEvent: update.awaitingEvent ?? null,\n ...(update.payload ? { payload: update.payload } : {}),\n },\n { merge: true },\n )\n .catch(() => undefined)\n}\n\n/**\n * Ends an enrollment by DELETING it.\n *\n * The row is live state, not a record. What happened to this person is\n * already written to `hosts/{hostId}/activity` — the same run history every\n * immediate action writes — so keeping a `done` row would be a second,\n * shorter answer to a question that is already answered, growing for ever in\n * a collection the sweep has to index.\n *\n * Deleting is also what lets the same person enter the same flow again: the\n * id is derived from the person, so a tombstone would be a permanent refusal\n * to ever run this sequence for them a second time.\n */\nexport async function endFlowEnrollment(\n ref: FirebaseFirestore.DocumentReference,\n): Promise<void> {\n await ref.delete().catch(() => undefined)\n}\n\n/**\n * The enrollments this person's event should wake, on this site.\n *\n * Keyed, bounded and cheap: three equality filters and a small limit, which\n * Firestore serves by merging single-field indexes. This is what keeps\n * `waitForEvent` off a polling design — nothing scans the enrolled population\n * looking for a match, the event arrives already knowing who it is about.\n *\n * The CALLER decides whether to ask at all. `runEventActions` fires on every\n * page view of every published site, so asking on every event would be a\n * query per visitor; it asks only when the payload names a person, which a\n * page view does not.\n */\nexport async function findFlowEnrollmentsAwaiting(options: {\n hostId: string\n event: string\n email: string\n firestore?: any\n limit?: number\n}): Promise<FirebaseFirestore.QueryDocumentSnapshot[]> {\n const key = personKey(options.email)\n const event = String(options.event ?? '').trim()\n if (!key || !event || !options.hostId) return []\n try {\n const snapshot = await enrollmentsRef(options.hostId, options.firestore)\n .where('awaitingEvent', '==', event)\n .where('personKey', '==', key)\n .where('status', '==', 'waiting')\n .limit(options.limit ?? 5)\n .get()\n return snapshot.docs\n } catch (error) {\n console.error('[flow] awaiting-event lookup failed', options.hostId, error)\n return []\n }\n}\n\n/** Where a partial sweep stopped, so the next beat resumes rather than restarts. */\nexport interface FlowSweepCursor {\n /** Full document path of the last enrollment read. */\n path: string\n}\n\nexport interface FlowSweepResult {\n /** Due enrollments read, against the budget. */\n scanned: number\n /** Enrollments this beat claimed and ran. */\n resumed: number\n /** Rows left untouched because their site is locked. */\n skippedLocked: number\n /** Rows another beat already held. */\n skippedClaimed: number\n /** False when the scan budget ran out — `cursor` says where to resume. */\n complete: boolean\n cursor: FlowSweepCursor | null\n}\n\n/**\n * One pass over the flows whose wait has ended.\n *\n * The RESUME itself is injected. Running a step needs the action executor,\n * which needs entitlements, datasets, webhooks and the mail path; keeping it\n * out of this function is what lets the scheduling contract — due-ness, the\n * claim, the budget, the lock — be exercised without any of that.\n */\nexport async function sweepDueFlowEnrollments(\n gate: PluginJobHostGate,\n options: {\n resume: (\n enrollment: FlowEnrollment,\n ref: FirebaseFirestore.DocumentReference,\n ) => Promise<void>\n nowMs?: number\n scanBudget?: number\n cursor?: FlowSweepCursor | null\n firestore?: any\n },\n): Promise<FlowSweepResult> {\n const firestore = options.firestore ?? firebaseAdmin.app().firestore()\n const nowMs = options.nowMs ?? Date.now()\n const budget = options.scanBudget ?? FLOW_RESUME_SCAN_BUDGET\n const result: FlowSweepResult = {\n scanned: 0,\n resumed: 0,\n skippedLocked: 0,\n skippedClaimed: 0,\n complete: true,\n cursor: null,\n }\n let after: FirebaseFirestore.DocumentSnapshot | null = options.cursor?.path\n ? await firestore\n .doc(options.cursor.path)\n .get()\n .catch(() => null)\n : null\n\n for (;;) {\n /*\n * Ordered by `resumeAtMs`, which every writer in this module sets on\n * every write. That is load-bearing twice over: an `orderBy` on a field\n * DROPS documents that lack it, so a writer that ever omitted it would\n * make those enrollments invisible to the only thing that resumes them;\n * and ordering oldest-due-first is what makes a budget fair, because the\n * work a short beat leaves behind is the newest rather than whichever\n * rows Firestore happened to return.\n */\n /*\n * The page is capped by what is LEFT of the budget, not only by\n * `PAGE_SIZE`. Checking the budget between pages would let a run whose\n * budget is smaller than one page read the whole page first — the budget\n * would then describe how much was reported rather than how much was\n * read, which is the opposite of what a read budget is for.\n */\n const remaining = budget - result.scanned\n if (remaining <= 0) break\n let query = firestore\n .collectionGroup(FLOW_ENROLLMENTS_SUBCOLLECTION)\n .where('status', '==', 'waiting')\n .where('resumeAtMs', '<=', nowMs)\n .orderBy('resumeAtMs')\n .limit(Math.min(PAGE_SIZE, remaining))\n if (after) query = query.startAfter(after)\n const page = await query.get()\n if (page.empty) break\n\n for (const doc of page.docs) {\n result.scanned += 1\n after = doc\n const hostId =\n String(doc.get('hostId') ?? '') || (doc.ref.parent.parent?.id ?? '')\n if (!hostId) continue\n /*\n * LOCKDOWN, first in the loop body and before any write. `continue`\n * leaves the row `waiting` and overdue, so the flow resumes on the\n * first beat after the lift rather than being cancelled by a pause.\n */\n if (await gate.isLocked(hostId)) {\n result.skippedLocked += 1\n continue\n }\n const enrollment = await claimFlowEnrollment(doc.ref, { nowMs })\n if (!enrollment) {\n result.skippedClaimed += 1\n continue\n }\n try {\n await options.resume(enrollment, doc.ref)\n result.resumed += 1\n } catch (error) {\n /*\n * One broken flow must not stop the sweep — the same isolation the\n * dynamic-list sweep gives each list. The claim is released so a\n * later beat retries; a permanently broken step therefore retries for\n * as long as the enrollment lives, which is bounded by its own wait\n * ceiling and is the recoverable direction.\n */\n console.error('[flow] resume failed', doc.ref.path, error)\n await deferFlowEnrollment(doc.ref, nowMs + FLOW_CLAIM_STALE_MS, nowMs)\n }\n }\n\n if (result.scanned >= budget) {\n result.complete = false\n result.cursor = after ? { path: after.ref.path } : null\n return result\n }\n if (page.size < Math.min(PAGE_SIZE, remaining)) break\n }\n return result\n}\n"],"names":["personKey","firebaseAdmin","FLOW_ENROLLMENTS_SUBCOLLECTION","FLOW_RESUME_SCAN_BUDGET","PAGE_SIZE","FLOW_CLAIM_STALE_MS","flowEnrollmentId","actionId","key","automation","enrollmentsRef","hostId","firestore","app","collection","doc","enrollInFlow","options","email","enrolled","reason","nowMs","Date","now","ref","runTransaction","transaction","existing","get","exists","status","claimedAtMs","Number","live","enrollment","actionName","String","action","name","resumeAtMs","nextStepIndex","steps","trim","toLowerCase","payload","event","enrolledAtMs","updatedAtMs","resumes","awaitingEvent","set","id","claimFlowEnrollment","fresh","claimable","update","data","deferFlowEnrollment","retryAtMs","merge","catch","undefined","advanceFlowEnrollment","FieldValue","increment","endFlowEnrollment","delete","findFlowEnrollmentsAwaiting","snapshot","where","limit","docs","error","console","sweepDueFlowEnrollments","gate","budget","scanBudget","result","scanned","resumed","skippedLocked","skippedClaimed","complete","cursor","after","path","remaining","query","collectionGroup","orderBy","Math","min","startAfter","page","empty","parent","isLocked","resume","size"],"mappings":"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"}
@@ -0,0 +1,232 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2026 Aglyn LLC
4
+ *
5
+ * Licensed under the Apache License, Version 2.0 (the "License");
6
+ * you may not use this file except in compliance with the License.
7
+ * You may obtain a copy of the License at
8
+ *
9
+ * http://www.apache.org/licenses/LICENSE-2.0
10
+ *
11
+ * Unless required by applicable law or agreed to in writing, software
12
+ * distributed under the License is distributed on an "AS IS" BASIS,
13
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ * See the License for the specific language governing permissions and
15
+ * limitations under the License.
16
+ */
17
+ import { type HostActionAlert, type HostFunction, type HostVariable, type HostWorkflow, type PluginJobHostGate } from '@aglyn/aglyn/server';
18
+ import { type FlowEnrollment, type FlowSweepCursor, type FlowSweepResult } from './flow-enrollments';
19
+ import type { HostEventPayload } from '@aglyn/tenant-runtime/host-event-listeners';
20
+ import { type AutomationWorkflow, type WorkflowStep } from './workflow-steps';
21
+ export interface ActionRunEnv {
22
+ hostId: string;
23
+ hostRef: FirebaseFirestore.DocumentReference;
24
+ alerts: HostActionAlert[];
25
+ /**
26
+ * Whether the owning org holds the actions builder (`actions`, Pro and up)
27
+ * — the gate every Actions step takes. An action run is admitted by it
28
+ * before its first step, so it is always true there; a workflow run is not,
29
+ * because its function calls need no such plan, so the workflow executor
30
+ * asks it of each Actions step it reaches.
31
+ */
32
+ actionsAllowed: boolean;
33
+ webhooksAllowed: boolean;
34
+ /**
35
+ * Whether the owning org holds the CRM suite (AGL-2611) — the gate the
36
+ * five CRM steps take, resolved once beside `webhooksAllowed` and for the
37
+ * same reason: a step gated on the org doc it already read costs no
38
+ * second read per step.
39
+ */
40
+ crmAllowed: boolean;
41
+ depth: number;
42
+ /**
43
+ * The owning org's billing doc, already read by the entitlement gate that
44
+ * admitted this run, and the org's id beside it.
45
+ *
46
+ * Carried rather than re-read: both entry points resolve `getOrgForHost`
47
+ * before they build this, so the dataset caps below cost no extra org read.
48
+ * Null only when the host has no resolvable org, which the gate treats as
49
+ * the free plan.
50
+ */
51
+ org: unknown;
52
+ orgId: string | null;
53
+ loadWorkflowContext: () => Promise<WorkflowContext>;
54
+ }
55
+ /**
56
+ * The site's functions, variables and workflows, double-keyed by document id
57
+ * and by name (AGL-261). A workflow value also carries its document id as
58
+ * `$id`, so a step that names a workflow by its name can still run it as the
59
+ * automation it is.
60
+ */
61
+ export interface WorkflowContext {
62
+ functions: Record<string, HostFunction>;
63
+ variables: Record<string, HostVariable>;
64
+ workflows: Record<string, HostWorkflow & {
65
+ $id?: string;
66
+ }>;
67
+ }
68
+ /**
69
+ * The automation a run belongs to, as the step executors need it: which kind,
70
+ * its document id, and its name for the history and the enrollment.
71
+ */
72
+ export interface AutomationRun {
73
+ kind: 'action' | 'workflow';
74
+ id: string;
75
+ name: string;
76
+ }
77
+ /**
78
+ * The run environment for one automation run: the org's plan gates resolved
79
+ * once, from the org document the caller already read.
80
+ */
81
+ export declare function automationRunEnv(input: {
82
+ hostId: string;
83
+ hostRef: FirebaseFirestore.DocumentReference;
84
+ owner: {
85
+ org?: unknown;
86
+ orgId?: string | null;
87
+ } | null | undefined;
88
+ depth: number;
89
+ alerts?: HostActionAlert[];
90
+ loadWorkflowContext?: ActionRunEnv['loadWorkflowContext'];
91
+ }): ActionRunEnv;
92
+ export declare function makeWorkflowContextLoader(hostRef: FirebaseFirestore.DocumentReference): () => Promise<WorkflowContext>;
93
+ /**
94
+ * The working set a run reads, from documents already in hand — for a door
95
+ * that read them itself and must not pay for them twice.
96
+ */
97
+ export declare function workflowContextFromDocs(functionDocs: readonly FirebaseFirestore.QueryDocumentSnapshot[], variableDocs: readonly FirebaseFirestore.QueryDocumentSnapshot[], workflowDocs: readonly FirebaseFirestore.QueryDocumentSnapshot[]): WorkflowContext;
98
+ /** How a run of an action's step list ended. */
99
+ type ActionRunEnding = 'ran' | 'waiting' | 'exited' | 'deferred';
100
+ /**
101
+ * What one Actions step did, for the run that holds it.
102
+ *
103
+ * - `skipped` — its `when` was not met, or it is a step the visitor's page
104
+ * runs; nothing is recorded.
105
+ * - `done` — it did its work; `detail` is the fact the history line carries.
106
+ * - `failed` — it did not; the run records the error and continues.
107
+ * - `exited` — an `exitFlow`: the run ends here.
108
+ * - `waiting` — a wait enrolled the person; the rest runs from the beat.
109
+ * - `halted` — a wait that could not enroll; the run ends with the error.
110
+ * - `deferred` — a resumed send was refused for now; the enrollment retries.
111
+ */
112
+ export type ServerStepVerdict = {
113
+ kind: 'skipped';
114
+ } | {
115
+ kind: 'done';
116
+ detail?: string;
117
+ } | {
118
+ kind: 'failed';
119
+ error: string;
120
+ } | {
121
+ kind: 'exited';
122
+ } | {
123
+ kind: 'waiting';
124
+ detail?: string;
125
+ } | {
126
+ kind: 'halted';
127
+ error: string;
128
+ } | {
129
+ kind: 'deferred';
130
+ };
131
+ /** How a workflow run went, for whoever records or answers for it. */
132
+ export interface WorkflowExecution {
133
+ ending: ActionRunEnding;
134
+ /**
135
+ * Every error, in step order. An Actions step's error is recorded and the
136
+ * run goes on, as it does in an action; a function call's error ENDS the
137
+ * run, as it always has, because the steps after it read its result.
138
+ */
139
+ errors: string[];
140
+ /** What each step that ran did, in the Actions run-history phrasing. */
141
+ outcomes: string[];
142
+ /** The workflow's return value, as the pure evaluator reports it. */
143
+ value: number | string | boolean;
144
+ /** Every result the function calls bound, by name. */
145
+ results: Record<string, number | string | boolean>;
146
+ }
147
+ interface ExecuteWorkflowOptions {
148
+ /** The step to start at: a resume's `nextStepIndex`. */
149
+ startIndex?: number;
150
+ /** The list to run: a resume's snapshot. */
151
+ steps?: readonly WorkflowStep[];
152
+ /** The enrollment a resume belongs to. */
153
+ enrollmentRef?: FirebaseFirestore.DocumentReference | null;
154
+ /** A run started by another automation's `runWorkflow` step. */
155
+ nested?: boolean;
156
+ }
157
+ /**
158
+ * Runs a workflow whose steps include Actions steps: function calls and
159
+ * Actions steps, in order, in one scope.
160
+ *
161
+ * - A FUNCTION CALL is evaluated by the platform's pure evaluator,
162
+ * `runWorkflow`, one call at a time, so its expressions see the event, the
163
+ * site's variables and every result bound before it — exactly the scope a
164
+ * function-only workflow gives it. A call that fails ends the run.
165
+ * - An ACTIONS STEP runs through {@link runServerStep}, the executor actions
166
+ * use, and reads the event payload together with every result bound so
167
+ * far: a workflow can compute a score and write it to a dataset. Each one
168
+ * takes the Actions tier gate (`actions`, Pro and up) on top of its own —
169
+ * `webhookPost` on Business, the CRM steps on the CRM suite — and a step
170
+ * only the visitor's browser can run is refused.
171
+ * - `wait` and `waitForEvent` enroll the person and end this call; the rest
172
+ * of the list, results included, continues from the beat. `exitFlow` ends
173
+ * the run.
174
+ *
175
+ * Records nothing and meters nothing: the caller that admitted the run does
176
+ * both, once.
177
+ */
178
+ export declare function executeWorkflow(env: ActionRunEnv, run: AutomationRun, workflow: AutomationWorkflow, event: string, payload: HostEventPayload, options?: ExecuteWorkflowOptions): Promise<WorkflowExecution>;
179
+ /**
180
+ * Event-triggered action runner (AGL-148): loads enabled actions whose
181
+ * `trigger.event` matches (built-in, site event, or custom), evaluates
182
+ * optional filters over the payload, and executes each step list in
183
+ * order. Never throws into the emitting request. Paid feature: the
184
+ * `actions` flag gates and `actionRunsPerMonth` meters runs.
185
+ */
186
+ export declare function runEventActions(hostId: string, event: string, payload?: HostEventPayload, depth?: number): Promise<HostActionAlert[]>;
187
+ /**
188
+ * Runs ONE action's server steps (AGL-256): the tenant page runtime
189
+ * evaluates site-event trigger conditions (scroll thresholds, selectors)
190
+ * client-side and dispatches the specific action here — re-matching by
191
+ * event name would wrongly fire sibling actions with different
192
+ * thresholds. Same gates and metering as the event runner.
193
+ */
194
+ export declare function runSingleAction(hostId: string, actionId: string, event: string, payload?: HostEventPayload): Promise<HostActionAlert[]>;
195
+ /**
196
+ * Ends an enrollment its automation may no longer run, and says why in the
197
+ * run history — the automation was deleted or switched off, or the plan that
198
+ * carried it lapsed. Shared by both kinds of enrollment, so "stopped mid-wait"
199
+ * reads the same for an action and a workflow.
200
+ */
201
+ export declare function stopFlowEnrollment(enrollment: FlowEnrollment, ref: FirebaseFirestore.DocumentReference, reason: string): Promise<'stopped'>;
202
+ /**
203
+ * Continues one enrollment from where its wait ended.
204
+ *
205
+ * The enrollment carries everything the run needs except the gates: the step
206
+ * list it entered with, the position inside it, the payload the trigger
207
+ * produced, and who it is about. What it deliberately does NOT carry is
208
+ * permission — the `actions` entitlement, the site's lockdown and the
209
+ * action's own enabled flag are all re-asked here, because a flow that waits
210
+ * three days is a flow that can outlive the plan, the site and the merchant's
211
+ * decision to run it.
212
+ */
213
+ export declare function resumeFlowEnrollment(enrollment: FlowEnrollment, ref: FirebaseFirestore.DocumentReference, options?: {
214
+ timedOut?: boolean;
215
+ nowMs?: number;
216
+ }): Promise<'ran' | 'waiting' | 'exited' | 'deferred' | 'stopped'>;
217
+ /** How long a deferred enrollment waits before the next attempt. */
218
+ export declare const FLOW_CLAIM_RETRY_MS: number;
219
+ /**
220
+ * The job beat's entry point: resume every flow whose wait has ended.
221
+ *
222
+ * Thin on purpose. The scheduling contract — due-ness, the transactional
223
+ * claim, the scan budget, the lockdown skip — is `sweepDueFlowEnrollments`,
224
+ * and the work is `resumeFlowEnrollment`; this is the wire between them, so
225
+ * neither has to import the other's dependencies to be tested.
226
+ */
227
+ export declare function runDueFlowEnrollments(gate: PluginJobHostGate, options?: {
228
+ nowMs?: number;
229
+ scanBudget?: number;
230
+ cursor?: FlowSweepCursor | null;
231
+ }): Promise<FlowSweepResult>;
232
+ export default runEventActions;