@aglyn/plugins-workflows 1.0.0-beta.218 → 1.0.0-beta.219

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 (52) hide show
  1. package/README.md +28 -4
  2. package/package.json +12 -12
  3. package/src/index.d.ts +1 -0
  4. package/src/index.js.map +1 -1
  5. package/src/lib/components/host-run-history-card.component.js +1 -1
  6. package/src/lib/components/host-run-history-card.component.js.map +1 -1
  7. package/src/lib/components/host-workflows-card.component.js +2 -1
  8. package/src/lib/components/host-workflows-card.component.js.map +1 -1
  9. package/src/lib/declarations.server.d.ts +6 -0
  10. package/src/lib/declarations.server.js +39 -0
  11. package/src/lib/declarations.server.js.map +1 -1
  12. package/src/lib/engine/run-event-actions.d.ts +2 -1
  13. package/src/lib/engine/run-event-actions.js +49 -15
  14. package/src/lib/engine/run-event-actions.js.map +1 -1
  15. package/src/lib/engine/run-event-workflows.js +2 -1
  16. package/src/lib/engine/run-event-workflows.js.map +1 -1
  17. package/src/lib/engine/workflow-steps.d.ts +1 -1
  18. package/src/lib/engine/workflow-steps.js +1 -1
  19. package/src/lib/engine/workflow-steps.js.map +1 -1
  20. package/src/lib/model/org-automations.js.map +1 -1
  21. package/src/lib/model/workflows-record-routes.d.ts +22 -0
  22. package/src/lib/model/workflows-record-routes.js +73 -0
  23. package/src/lib/model/workflows-record-routes.js.map +1 -0
  24. package/src/lib/model/workflows-search-sources.d.ts +20 -0
  25. package/src/lib/model/workflows-search-sources.js +35 -0
  26. package/src/lib/model/workflows-search-sources.js.map +1 -0
  27. package/src/lib/model/workflows.d.ts +90 -0
  28. package/src/lib/model/workflows.js +166 -0
  29. package/src/lib/model/workflows.js.map +1 -0
  30. package/src/lib/plan-entitlements.d.ts +36 -0
  31. package/src/lib/plan-entitlements.js +44 -0
  32. package/src/lib/plan-entitlements.js.map +1 -0
  33. package/src/lib/plugin.js +5 -1
  34. package/src/lib/plugin.js.map +1 -1
  35. package/src/lib/server/automation-record-index.d.ts +20 -0
  36. package/src/lib/server/automation-record-index.js +90 -0
  37. package/src/lib/server/automation-record-index.js.map +1 -0
  38. package/src/lib/server/workflow-dependents.d.ts +28 -0
  39. package/src/lib/server/workflow-dependents.js +61 -0
  40. package/src/lib/server/workflow-dependents.js.map +1 -0
  41. package/src/lib/server/workflow-variables.d.ts +20 -0
  42. package/src/lib/server/workflow-variables.js +75 -0
  43. package/src/lib/server/workflow-variables.js.map +1 -0
  44. package/src/lib/server.d.ts +1 -0
  45. package/src/lib/server.js +2 -1
  46. package/src/lib/server.js.map +1 -1
  47. package/src/lib/tenant-emails.d.ts +29 -0
  48. package/src/lib/tenant-emails.js +41 -0
  49. package/src/lib/tenant-emails.js.map +1 -0
  50. package/src/lib/usage-axes.d.ts +28 -0
  51. package/src/lib/usage-axes.js +61 -0
  52. package/src/lib/usage-axes.js.map +1 -0
@@ -14,11 +14,12 @@ import { _ as _extends } from "@swc/helpers/_/_extends";
14
14
  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
15
  * See the License for the specific language governing permissions and
16
16
  * limitations under the License.
17
- */ import { ACTION_MAX_EVENT_DEPTH, checkEntitlement, evaluateExpression, FLOW_TIMED_OUT_FIELD, resolveOrgEntitlements, runWorkflow } from "@aglyn/aglyn/server";
17
+ */ import { ACTION_MAX_EVENT_DEPTH, checkEntitlement, evaluateExpression, FLOW_TIMED_OUT_FIELD, resolveOrgEntitlements } from "@aglyn/aglyn/server";
18
18
  import { activitySearchTokens } from "@aglyn/aglyn/app-utils/activity-search";
19
19
  import { firebaseAdmin, getOrgForHost } from "@aglyn/tenant-data-admin";
20
20
  import { FieldValue } from "firebase-admin/firestore";
21
21
  import { runSummaryFields } from "../model/run-history.js";
22
+ import { runWorkflow } from "../model/workflows.js";
22
23
  import { eventRunSuspension } from "./site-suspension.js";
23
24
  import { deferFlowEnrollment, endFlowEnrollment } from "./flow-enrollments.js";
24
25
  import { automationRunEnv, executeWorkflow, FLOW_CLAIM_RETRY_MS, stopFlowEnrollment } from "./run-event-actions.js";
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/plugins/workflows/src/lib/engine/run-event-workflows.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\nimport {\n ACTION_MAX_EVENT_DEPTH,\n checkEntitlement,\n evaluateExpression,\n FLOW_TIMED_OUT_FIELD,\n type HostActionAlert,\n type HostEventType,\n type HostFunction,\n type HostVariable,\n type HostWorkflow,\n resolveOrgEntitlements,\n runWorkflow,\n} from '@aglyn/aglyn/server'\nimport { activitySearchTokens } from '@aglyn/aglyn/app-utils/activity-search'\nimport { firebaseAdmin, getOrgForHost } from '@aglyn/tenant-data-admin'\nimport type { HostEventPayload } from '@aglyn/tenant-runtime/host-event-listeners'\nimport { FieldValue } from 'firebase-admin/firestore'\nimport { runSummaryFields } from '../model/run-history'\nimport { eventRunSuspension } from './site-suspension'\nimport {\n deferFlowEnrollment,\n endFlowEnrollment,\n type FlowEnrollment,\n} from './flow-enrollments'\nimport {\n automationRunEnv,\n executeWorkflow,\n FLOW_CLAIM_RETRY_MS,\n stopFlowEnrollment,\n type WorkflowContext,\n type WorkflowExecution,\n} from './run-event-actions'\nimport {\n type AutomationWorkflow,\n workflowHasActionSteps,\n} from './workflow-steps'\nimport { runTriggeredByFields } from './run-trigger-actor'\n\n/** Bounded fan-out per event: at most this many triggered workflows run. */\nconst MAX_TRIGGERED_WORKFLOWS = 10\n\n/**\n * One workflow run's row in the site's activity feed, which is the run\n * history the Workflows tab reads.\n *\n * The one place a workflow's run is written, whichever door started it.\n */\nasync function recordWorkflowRun(\n hostRef: FirebaseFirestore.DocumentReference,\n row: Record<string, unknown>,\n): Promise<void> {\n await hostRef\n .collection('activity')\n .add({\n actorId: null,\n actorEmail: null,\n // Who set this run off (AGL-3376).\n ...runTriggeredByFields(),\n ...row,\n // The site log's search finds a run by its workflow's name (AGL-3321).\n searchTokens: activitySearchTokens({\n target: row['target'] as { name?: unknown } | undefined,\n }),\n createdAt: FieldValue.serverTimestamp(),\n })\n .catch(() => undefined)\n}\n\n/**\n * The history row for a run of a workflow with Actions steps, in the Actions\n * run's phrasing (AGL-2171) — what each step did, joined — so the run table\n * shows every step's outcome whichever engine the step came from. `status`\n * and `durationMs` ride along as they do on every workflow run, for the\n * dashboard feed that renders them.\n */\nexport function workflowRunRow(\n execution: Pick<WorkflowExecution, 'ending' | 'errors' | 'outcomes'>,\n input: {\n event: string\n workflowId: string\n workflowName: string\n durationMs: number\n },\n): Record<string, unknown> {\n const failed = execution.errors.length > 0\n const action = failed\n ? `Workflow ran on ${input.event} with errors: ${execution.errors.join('; ')}`.slice(\n 0,\n 300,\n )\n : execution.ending === 'waiting'\n ? `Workflow is waiting, on ${input.event}`\n : `Workflow ran on ${input.event}`\n return {\n action,\n result: failed ? 'failed' : 'succeeded',\n trigger: input.event,\n // The summary and its search tokens, written together (AGL-3321).\n ...runSummaryFields(\n (execution.outcomes.length\n ? execution.outcomes.join(' · ')\n : 'Ran'\n ).slice(0, 300),\n ),\n status: failed ? 'error' : 'ok',\n durationMs: input.durationMs,\n target: {\n type: 'workflow',\n id: input.workflowId,\n name: input.workflowName,\n },\n }\n}\n\n/**\n * Event-triggered workflow runner (AGL-128): loads workflows whose\n * `trigger.event` matches, evaluates optional trigger filters over the\n * event payload, runs each, and logs runs to the host activity feed.\n * Fire-and-forget from the tenant's event sites (form submit, pageview\n * collector, membership APIs) — it never throws, and a failure must not\n * break the request that emitted the event.\n *\n * A workflow of function calls runs through the pure `runWorkflow`\n * evaluator exactly as it always has. A workflow with Actions steps runs\n * through `executeWorkflow`, which performs them with the Actions executors;\n * the site alerts its steps raise are what this resolves.\n *\n * ONE RUN, METERED ONCE: either kind counts one run on the workflow meter,\n * `workflowRunsPerMonth`, however many steps it has and whatever they are.\n * Its Actions steps take the Actions tier gates step by step, and none of\n * them counts an action run.\n *\n * `depth` is the chain guard actions run under (`ACTION_MAX_EVENT_DEPTH`):\n * an event a workflow's own step raised — a stage it set — is heard one\n * level deeper, so a workflow that sets the stage it listens for stops.\n */\nexport async function runEventWorkflows(\n hostId: string,\n event: HostEventType,\n payload: HostEventPayload = {},\n depth = 0,\n): Promise<HostActionAlert[]> {\n const alerts: HostActionAlert[] = []\n if (depth > ACTION_MAX_EVENT_DEPTH) return alerts\n try {\n const firestore = firebaseAdmin.app().firestore()\n const hostRef = firestore.collection('hosts').doc(hostId)\n const triggered = await hostRef\n .collection('workflows')\n .where('trigger.event', '==', event)\n .limit(MAX_TRIGGERED_WORKFLOWS)\n .get()\n const workflows = triggered.docs.filter((doc) => !doc.get('deletedAt'))\n if (!workflows.length) return alerts\n // Full workflow map for nested function→workflow calls (AGL-129).\n const allWorkflowDocs = await hostRef\n .collection('workflows')\n .limit(100)\n .get()\n // Double-keyed by doc id AND name (AGL-261): id references are\n // rename-safe; legacy name references keep resolving.\n // Each carries its id, so a workflow a step names by its legacy name\n // still runs as the automation it is (see `WorkflowContext`).\n const workflowMap: WorkflowContext['workflows'] = {}\n for (const doc of allWorkflowDocs.docs) {\n const data = doc.data() as any\n if (data.deletedAt) continue\n const withId = { ...data, $id: doc.id }\n workflowMap[doc.id] = withId\n if (data?.name) workflowMap[data.name] = withId\n }\n\n // Monthly run cap by the owning org's plan (AGL-165) — dark-launch\n // rule: workspaces without a plan are uncapped, like every other gate.\n const monthKey = new Date().toISOString().slice(0, 7)\n const runCounterRef = hostRef.collection('counters').doc('workflowRuns')\n // Run caps ride the owning org's doc (AGL-238) — read through\n // `getOrgForHost` below. The host document itself is not consulted: the\n // cap, the counter and the workflow definitions all live elsewhere, so a\n // read of it here would be billed on every host event and used for\n // nothing.\n const owner = await getOrgForHost(hostId)\n // A suspended site runs nothing (AGL-3356): see `eventRunSuspension`.\n // Asked before the cap so a locked site's events bill no runs.\n if (await eventRunSuspension(hostRef, owner?.org)) return alerts\n {\n const org = owner?.org\n // Plan-less orgs resolve as free (AGL-247) — the cap always runs.\n const limit = resolveOrgEntitlements(\n org as any,\n ).workflowRunsPerMonth\n const counterSnapshot = await runCounterRef.get()\n const used = Number(counterSnapshot.get(monthKey) ?? 0)\n if (used + workflows.length > limit) return alerts\n }\n\n const [functionDocs, variableDocs] = await Promise.all([\n hostRef.collection('functions').limit(100).get(),\n hostRef.collection('variables').limit(100).get(),\n ])\n const functions: Record<string, HostFunction> = {}\n for (const doc of functionDocs.docs) {\n const definition = doc.data() as any\n if (definition.deletedAt) continue\n functions[doc.id] = definition\n if (definition.name) functions[definition.name] = definition\n }\n const variables: Record<string, HostVariable> = {}\n for (const doc of variableDocs.docs) {\n const variable = doc.data() as any\n if (!variable.deletedAt && variable.name) {\n variables[variable.name] = variable\n }\n }\n\n let executed = 0\n for (const doc of workflows) {\n const workflow = doc.data() as AutomationWorkflow\n const filter = workflow.trigger?.filter?.trim()\n if (filter) {\n try {\n const scope: HostEventPayload = { event, ...payload }\n if (!evaluateExpression(filter, scope)) continue\n } catch {\n continue // A broken filter never fires.\n }\n }\n const startedAt = Date.now()\n if (workflowHasActionSteps(workflow)) {\n /*\n * A WORKFLOW WITH ACTIONS STEPS is performed, not evaluated. The\n * maps above are this run's working set, so the executor reads them\n * rather than a second copy; the plan gates come off the org\n * document the cap already read.\n */\n const context: WorkflowContext = {\n functions,\n variables,\n workflows: workflowMap,\n }\n const env = automationRunEnv({\n hostId,\n hostRef,\n owner,\n depth,\n alerts,\n loadWorkflowContext: async () => context,\n })\n const execution = await executeWorkflow(\n env,\n { kind: 'workflow', id: doc.id, name: workflow.name ?? '' },\n workflow,\n event,\n payload,\n )\n executed += 1\n await recordWorkflowRun(\n hostRef,\n workflowRunRow(execution, {\n event,\n workflowId: doc.id,\n workflowName: workflow.name ?? '',\n durationMs: Date.now() - startedAt,\n }),\n )\n continue\n }\n const run = runWorkflow(\n workflow as HostWorkflow,\n functions,\n variables,\n { event, ...payload },\n { workflows: workflowMap },\n )\n const durationMs = Date.now() - startedAt\n executed += 1\n // `=== false` (not `!run.ok`): the union fails to narrow under the\n // stricter build tsconfig otherwise (same quirk as runWorkflow).\n const action =\n run.ok === false\n ? `Workflow failed on ${event}: ${run.error}`.slice(0, 300)\n : `Workflow ran on ${event}`\n /*\n * The run-history shape, not just `status` + `durationMs` (AGL-2222).\n *\n * `actionRunResult()` recognises a run by `result`, falling back to the\n * prose prefix `Action ran on`. This writer's prose begins\n * `Workflow ran on`, so every workflow execution was filtered out of the\n * very table AGL-2171 built — the Runs dialog on the Workflows tab read\n * \"No runs yet\" however many times a workflow had run. The `durationMs`\n * caption in that table could never render either, because the only rows\n * carrying a duration were the ones being dropped.\n *\n * `status` stays: the dashboard activity feed renders it, and removing\n * it would trade one silent gap for another.\n */\n await recordWorkflowRun(hostRef, {\n action,\n result: run.ok === false ? 'failed' : 'succeeded',\n trigger: event,\n ...runSummaryFields(\n run.ok === false ? String(run.error).slice(0, 300) : 'Ran',\n ),\n status: run.ok === false ? 'error' : 'ok',\n durationMs,\n target: { type: 'workflow', id: doc.id, name: workflow.name ?? '' },\n })\n }\n // Filtered-out workflows do not bill — only executed runs count.\n if (executed > 0) {\n await runCounterRef\n .set(\n { [monthKey]: FieldValue.increment(executed) },\n { merge: true },\n )\n .catch(() => undefined)\n }\n } catch (error) {\n console.error('runEventWorkflows failed', hostId, event, error)\n }\n return alerts\n}\n\n/**\n * Continues a WORKFLOW's enrollment from where its wait ended — the\n * workflow twin of the action resume, and asked the same questions.\n *\n * - The kill switch is the workflow document: deleted, and the people inside\n * it stop, exactly as they do when an action is deleted or switched off.\n * - The plan is re-asked: a wait is an Actions step, so a workspace whose\n * plan has lost the actions builder stops the flow rather than finishing\n * it on a plan that no longer carries it.\n * - The snapshot runs, not the workflow's current steps, and the results the\n * function calls bound before the wait ride in its payload.\n * - Counted on the workflow meter, never refused by it: the person is already\n * inside, and the gate belongs at enrollment.\n */\nexport async function resumeWorkflowEnrollment(\n enrollment: FlowEnrollment,\n ref: FirebaseFirestore.DocumentReference,\n options: { timedOut?: boolean; nowMs: number },\n): Promise<'ran' | 'waiting' | 'exited' | 'deferred' | 'stopped'> {\n const { nowMs } = options\n const hostId = enrollment.hostId\n const hostRef = firebaseAdmin.app().firestore().collection('hosts').doc(hostId)\n const doc = await hostRef\n .collection('workflows')\n .doc(enrollment.actionId)\n .get()\n .catch(() => null)\n if (!doc?.exists || doc.get('deletedAt')) {\n return await stopFlowEnrollment(\n enrollment,\n ref,\n 'the workflow was deleted',\n )\n }\n const workflow = doc.data() as AutomationWorkflow\n const owner = await getOrgForHost(hostId).catch(() => null)\n if (!checkEntitlement(owner?.org as any, 'actions')) {\n return await stopFlowEnrollment(\n enrollment,\n ref,\n 'this site’s plan no longer includes automations',\n )\n }\n const env = automationRunEnv({ hostId, hostRef, owner, depth: 0 })\n const payload: HostEventPayload = {\n ...((enrollment.payload ?? {}) as HostEventPayload),\n // The timeout BRANCH, as on an action's resume: the step after a\n // `waitForEvent` reads it to tell the clock from the event.\n ...(options.timedOut ? { [FLOW_TIMED_OUT_FIELD]: true } : {}),\n }\n const startedAt = Date.now()\n const execution = await executeWorkflow(\n env,\n { kind: 'workflow', id: doc.id, name: workflow.name ?? '' },\n workflow,\n enrollment.event,\n payload,\n {\n startIndex: enrollment.nextStepIndex,\n steps: enrollment.steps,\n enrollmentRef: ref,\n },\n )\n if (execution.ending === 'deferred') {\n await deferFlowEnrollment(ref, nowMs + FLOW_CLAIM_RETRY_MS, nowMs)\n return execution.ending\n }\n if (execution.ending !== 'waiting') await endFlowEnrollment(ref)\n await recordWorkflowRun(\n hostRef,\n workflowRunRow(execution, {\n event: enrollment.event,\n workflowId: doc.id,\n workflowName: workflow.name ?? '',\n durationMs: Date.now() - startedAt,\n }),\n )\n const monthKey = new Date(nowMs).toISOString().slice(0, 7)\n await hostRef\n .collection('counters')\n .doc('workflowRuns')\n .set({ [monthKey]: FieldValue.increment(1) }, { merge: true })\n .catch(() => undefined)\n return execution.ending\n}\n\nexport default runEventWorkflows\n"],"names":["ACTION_MAX_EVENT_DEPTH","checkEntitlement","evaluateExpression","FLOW_TIMED_OUT_FIELD","resolveOrgEntitlements","runWorkflow","activitySearchTokens","firebaseAdmin","getOrgForHost","FieldValue","runSummaryFields","eventRunSuspension","deferFlowEnrollment","endFlowEnrollment","automationRunEnv","executeWorkflow","FLOW_CLAIM_RETRY_MS","stopFlowEnrollment","workflowHasActionSteps","runTriggeredByFields","MAX_TRIGGERED_WORKFLOWS","recordWorkflowRun","hostRef","row","collection","add","actorId","actorEmail","searchTokens","target","createdAt","serverTimestamp","catch","undefined","workflowRunRow","execution","input","failed","errors","length","action","event","join","slice","ending","result","trigger","outcomes","status","durationMs","type","id","workflowId","name","workflowName","runEventWorkflows","hostId","payload","depth","alerts","firestore","app","doc","triggered","where","limit","get","workflows","docs","filter","allWorkflowDocs","workflowMap","data","deletedAt","withId","$id","monthKey","Date","toISOString","runCounterRef","owner","org","counterSnapshot","workflowRunsPerMonth","used","Number","functionDocs","variableDocs","Promise","all","functions","definition","variables","variable","executed","workflow","trim","scope","startedAt","now","context","env","loadWorkflowContext","kind","run","ok","error","String","set","increment","merge","console","resumeWorkflowEnrollment","enrollment","ref","options","nowMs","actionId","exists","timedOut","startIndex","nextStepIndex","steps","enrollmentRef"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/workflows/src/lib/engine/run-event-workflows.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\nimport {\n ACTION_MAX_EVENT_DEPTH,\n checkEntitlement,\n evaluateExpression,\n FLOW_TIMED_OUT_FIELD,\n type HostActionAlert,\n type HostEventType,\n type HostFunction,\n type HostVariable,\n resolveOrgEntitlements,\n} from '@aglyn/aglyn/server'\nimport { activitySearchTokens } from '@aglyn/aglyn/app-utils/activity-search'\nimport { firebaseAdmin, getOrgForHost } from '@aglyn/tenant-data-admin'\nimport type { HostEventPayload } from '@aglyn/tenant-runtime/host-event-listeners'\nimport { FieldValue } from 'firebase-admin/firestore'\nimport { runSummaryFields } from '../model/run-history'\nimport { type HostWorkflow, runWorkflow } from '../model/workflows'\nimport { eventRunSuspension } from './site-suspension'\nimport {\n deferFlowEnrollment,\n endFlowEnrollment,\n type FlowEnrollment,\n} from './flow-enrollments'\nimport {\n automationRunEnv,\n executeWorkflow,\n FLOW_CLAIM_RETRY_MS,\n stopFlowEnrollment,\n type WorkflowContext,\n type WorkflowExecution,\n} from './run-event-actions'\nimport {\n type AutomationWorkflow,\n workflowHasActionSteps,\n} from './workflow-steps'\nimport { runTriggeredByFields } from './run-trigger-actor'\n\n/** Bounded fan-out per event: at most this many triggered workflows run. */\nconst MAX_TRIGGERED_WORKFLOWS = 10\n\n/**\n * One workflow run's row in the site's activity feed, which is the run\n * history the Workflows tab reads.\n *\n * The one place a workflow's run is written, whichever door started it.\n */\nasync function recordWorkflowRun(\n hostRef: FirebaseFirestore.DocumentReference,\n row: Record<string, unknown>,\n): Promise<void> {\n await hostRef\n .collection('activity')\n .add({\n actorId: null,\n actorEmail: null,\n // Who set this run off (AGL-3376).\n ...runTriggeredByFields(),\n ...row,\n // The site log's search finds a run by its workflow's name (AGL-3321).\n searchTokens: activitySearchTokens({\n target: row['target'] as { name?: unknown } | undefined,\n }),\n createdAt: FieldValue.serverTimestamp(),\n })\n .catch(() => undefined)\n}\n\n/**\n * The history row for a run of a workflow with Actions steps, in the Actions\n * run's phrasing (AGL-2171) — what each step did, joined — so the run table\n * shows every step's outcome whichever engine the step came from. `status`\n * and `durationMs` ride along as they do on every workflow run, for the\n * dashboard feed that renders them.\n */\nexport function workflowRunRow(\n execution: Pick<WorkflowExecution, 'ending' | 'errors' | 'outcomes'>,\n input: {\n event: string\n workflowId: string\n workflowName: string\n durationMs: number\n },\n): Record<string, unknown> {\n const failed = execution.errors.length > 0\n const action = failed\n ? `Workflow ran on ${input.event} with errors: ${execution.errors.join('; ')}`.slice(\n 0,\n 300,\n )\n : execution.ending === 'waiting'\n ? `Workflow is waiting, on ${input.event}`\n : `Workflow ran on ${input.event}`\n return {\n action,\n result: failed ? 'failed' : 'succeeded',\n trigger: input.event,\n // The summary and its search tokens, written together (AGL-3321).\n ...runSummaryFields(\n (execution.outcomes.length\n ? execution.outcomes.join(' · ')\n : 'Ran'\n ).slice(0, 300),\n ),\n status: failed ? 'error' : 'ok',\n durationMs: input.durationMs,\n target: {\n type: 'workflow',\n id: input.workflowId,\n name: input.workflowName,\n },\n }\n}\n\n/**\n * Event-triggered workflow runner (AGL-128): loads workflows whose\n * `trigger.event` matches, evaluates optional trigger filters over the\n * event payload, runs each, and logs runs to the host activity feed.\n * Fire-and-forget from the tenant's event sites (form submit, pageview\n * collector, membership APIs) — it never throws, and a failure must not\n * break the request that emitted the event.\n *\n * A workflow of function calls runs through the pure `runWorkflow`\n * evaluator exactly as it always has. A workflow with Actions steps runs\n * through `executeWorkflow`, which performs them with the Actions executors;\n * the site alerts its steps raise are what this resolves.\n *\n * ONE RUN, METERED ONCE: either kind counts one run on the workflow meter,\n * `workflowRunsPerMonth`, however many steps it has and whatever they are.\n * Its Actions steps take the Actions tier gates step by step, and none of\n * them counts an action run.\n *\n * `depth` is the chain guard actions run under (`ACTION_MAX_EVENT_DEPTH`):\n * an event a workflow's own step raised — a stage it set — is heard one\n * level deeper, so a workflow that sets the stage it listens for stops.\n */\nexport async function runEventWorkflows(\n hostId: string,\n event: HostEventType,\n payload: HostEventPayload = {},\n depth = 0,\n): Promise<HostActionAlert[]> {\n const alerts: HostActionAlert[] = []\n if (depth > ACTION_MAX_EVENT_DEPTH) return alerts\n try {\n const firestore = firebaseAdmin.app().firestore()\n const hostRef = firestore.collection('hosts').doc(hostId)\n const triggered = await hostRef\n .collection('workflows')\n .where('trigger.event', '==', event)\n .limit(MAX_TRIGGERED_WORKFLOWS)\n .get()\n const workflows = triggered.docs.filter((doc) => !doc.get('deletedAt'))\n if (!workflows.length) return alerts\n // Full workflow map for nested function→workflow calls (AGL-129).\n const allWorkflowDocs = await hostRef\n .collection('workflows')\n .limit(100)\n .get()\n // Double-keyed by doc id AND name (AGL-261): id references are\n // rename-safe; legacy name references keep resolving.\n // Each carries its id, so a workflow a step names by its legacy name\n // still runs as the automation it is (see `WorkflowContext`).\n const workflowMap: WorkflowContext['workflows'] = {}\n for (const doc of allWorkflowDocs.docs) {\n const data = doc.data() as any\n if (data.deletedAt) continue\n const withId = { ...data, $id: doc.id }\n workflowMap[doc.id] = withId\n if (data?.name) workflowMap[data.name] = withId\n }\n\n // Monthly run cap by the owning org's plan (AGL-165) — dark-launch\n // rule: workspaces without a plan are uncapped, like every other gate.\n const monthKey = new Date().toISOString().slice(0, 7)\n const runCounterRef = hostRef.collection('counters').doc('workflowRuns')\n // Run caps ride the owning org's doc (AGL-238) — read through\n // `getOrgForHost` below. The host document itself is not consulted: the\n // cap, the counter and the workflow definitions all live elsewhere, so a\n // read of it here would be billed on every host event and used for\n // nothing.\n const owner = await getOrgForHost(hostId)\n // A suspended site runs nothing (AGL-3356): see `eventRunSuspension`.\n // Asked before the cap so a locked site's events bill no runs.\n if (await eventRunSuspension(hostRef, owner?.org)) return alerts\n {\n const org = owner?.org\n // Plan-less orgs resolve as free (AGL-247) — the cap always runs.\n const limit = resolveOrgEntitlements(\n org as any,\n ).workflowRunsPerMonth\n const counterSnapshot = await runCounterRef.get()\n const used = Number(counterSnapshot.get(monthKey) ?? 0)\n if (used + workflows.length > limit) return alerts\n }\n\n const [functionDocs, variableDocs] = await Promise.all([\n hostRef.collection('functions').limit(100).get(),\n hostRef.collection('variables').limit(100).get(),\n ])\n const functions: Record<string, HostFunction> = {}\n for (const doc of functionDocs.docs) {\n const definition = doc.data() as any\n if (definition.deletedAt) continue\n functions[doc.id] = definition\n if (definition.name) functions[definition.name] = definition\n }\n const variables: Record<string, HostVariable> = {}\n for (const doc of variableDocs.docs) {\n const variable = doc.data() as any\n if (!variable.deletedAt && variable.name) {\n variables[variable.name] = variable\n }\n }\n\n let executed = 0\n for (const doc of workflows) {\n const workflow = doc.data() as AutomationWorkflow\n const filter = workflow.trigger?.filter?.trim()\n if (filter) {\n try {\n const scope: HostEventPayload = { event, ...payload }\n if (!evaluateExpression(filter, scope)) continue\n } catch {\n continue // A broken filter never fires.\n }\n }\n const startedAt = Date.now()\n if (workflowHasActionSteps(workflow)) {\n /*\n * A WORKFLOW WITH ACTIONS STEPS is performed, not evaluated. The\n * maps above are this run's working set, so the executor reads them\n * rather than a second copy; the plan gates come off the org\n * document the cap already read.\n */\n const context: WorkflowContext = {\n functions,\n variables,\n workflows: workflowMap,\n }\n const env = automationRunEnv({\n hostId,\n hostRef,\n owner,\n depth,\n alerts,\n loadWorkflowContext: async () => context,\n })\n const execution = await executeWorkflow(\n env,\n { kind: 'workflow', id: doc.id, name: workflow.name ?? '' },\n workflow,\n event,\n payload,\n )\n executed += 1\n await recordWorkflowRun(\n hostRef,\n workflowRunRow(execution, {\n event,\n workflowId: doc.id,\n workflowName: workflow.name ?? '',\n durationMs: Date.now() - startedAt,\n }),\n )\n continue\n }\n const run = runWorkflow(\n workflow as HostWorkflow,\n functions,\n variables,\n { event, ...payload },\n { workflows: workflowMap },\n )\n const durationMs = Date.now() - startedAt\n executed += 1\n // `=== false` (not `!run.ok`): the union fails to narrow under the\n // stricter build tsconfig otherwise (same quirk as runWorkflow).\n const action =\n run.ok === false\n ? `Workflow failed on ${event}: ${run.error}`.slice(0, 300)\n : `Workflow ran on ${event}`\n /*\n * The run-history shape, not just `status` + `durationMs` (AGL-2222).\n *\n * `actionRunResult()` recognises a run by `result`, falling back to the\n * prose prefix `Action ran on`. This writer's prose begins\n * `Workflow ran on`, so every workflow execution was filtered out of the\n * very table AGL-2171 built — the Runs dialog on the Workflows tab read\n * \"No runs yet\" however many times a workflow had run. The `durationMs`\n * caption in that table could never render either, because the only rows\n * carrying a duration were the ones being dropped.\n *\n * `status` stays: the dashboard activity feed renders it, and removing\n * it would trade one silent gap for another.\n */\n await recordWorkflowRun(hostRef, {\n action,\n result: run.ok === false ? 'failed' : 'succeeded',\n trigger: event,\n ...runSummaryFields(\n run.ok === false ? String(run.error).slice(0, 300) : 'Ran',\n ),\n status: run.ok === false ? 'error' : 'ok',\n durationMs,\n target: { type: 'workflow', id: doc.id, name: workflow.name ?? '' },\n })\n }\n // Filtered-out workflows do not bill — only executed runs count.\n if (executed > 0) {\n await runCounterRef\n .set(\n { [monthKey]: FieldValue.increment(executed) },\n { merge: true },\n )\n .catch(() => undefined)\n }\n } catch (error) {\n console.error('runEventWorkflows failed', hostId, event, error)\n }\n return alerts\n}\n\n/**\n * Continues a WORKFLOW's enrollment from where its wait ended — the\n * workflow twin of the action resume, and asked the same questions.\n *\n * - The kill switch is the workflow document: deleted, and the people inside\n * it stop, exactly as they do when an action is deleted or switched off.\n * - The plan is re-asked: a wait is an Actions step, so a workspace whose\n * plan has lost the actions builder stops the flow rather than finishing\n * it on a plan that no longer carries it.\n * - The snapshot runs, not the workflow's current steps, and the results the\n * function calls bound before the wait ride in its payload.\n * - Counted on the workflow meter, never refused by it: the person is already\n * inside, and the gate belongs at enrollment.\n */\nexport async function resumeWorkflowEnrollment(\n enrollment: FlowEnrollment,\n ref: FirebaseFirestore.DocumentReference,\n options: { timedOut?: boolean; nowMs: number },\n): Promise<'ran' | 'waiting' | 'exited' | 'deferred' | 'stopped'> {\n const { nowMs } = options\n const hostId = enrollment.hostId\n const hostRef = firebaseAdmin.app().firestore().collection('hosts').doc(hostId)\n const doc = await hostRef\n .collection('workflows')\n .doc(enrollment.actionId)\n .get()\n .catch(() => null)\n if (!doc?.exists || doc.get('deletedAt')) {\n return await stopFlowEnrollment(\n enrollment,\n ref,\n 'the workflow was deleted',\n )\n }\n const workflow = doc.data() as AutomationWorkflow\n const owner = await getOrgForHost(hostId).catch(() => null)\n if (!checkEntitlement(owner?.org as any, 'actions')) {\n return await stopFlowEnrollment(\n enrollment,\n ref,\n 'this site’s plan no longer includes automations',\n )\n }\n const env = automationRunEnv({ hostId, hostRef, owner, depth: 0 })\n const payload: HostEventPayload = {\n ...((enrollment.payload ?? {}) as HostEventPayload),\n // The timeout BRANCH, as on an action's resume: the step after a\n // `waitForEvent` reads it to tell the clock from the event.\n ...(options.timedOut ? { [FLOW_TIMED_OUT_FIELD]: true } : {}),\n }\n const startedAt = Date.now()\n const execution = await executeWorkflow(\n env,\n { kind: 'workflow', id: doc.id, name: workflow.name ?? '' },\n workflow,\n enrollment.event,\n payload,\n {\n startIndex: enrollment.nextStepIndex,\n steps: enrollment.steps,\n enrollmentRef: ref,\n },\n )\n if (execution.ending === 'deferred') {\n await deferFlowEnrollment(ref, nowMs + FLOW_CLAIM_RETRY_MS, nowMs)\n return execution.ending\n }\n if (execution.ending !== 'waiting') await endFlowEnrollment(ref)\n await recordWorkflowRun(\n hostRef,\n workflowRunRow(execution, {\n event: enrollment.event,\n workflowId: doc.id,\n workflowName: workflow.name ?? '',\n durationMs: Date.now() - startedAt,\n }),\n )\n const monthKey = new Date(nowMs).toISOString().slice(0, 7)\n await hostRef\n .collection('counters')\n .doc('workflowRuns')\n .set({ [monthKey]: FieldValue.increment(1) }, { merge: true })\n .catch(() => undefined)\n return execution.ending\n}\n\nexport default runEventWorkflows\n"],"names":["ACTION_MAX_EVENT_DEPTH","checkEntitlement","evaluateExpression","FLOW_TIMED_OUT_FIELD","resolveOrgEntitlements","activitySearchTokens","firebaseAdmin","getOrgForHost","FieldValue","runSummaryFields","runWorkflow","eventRunSuspension","deferFlowEnrollment","endFlowEnrollment","automationRunEnv","executeWorkflow","FLOW_CLAIM_RETRY_MS","stopFlowEnrollment","workflowHasActionSteps","runTriggeredByFields","MAX_TRIGGERED_WORKFLOWS","recordWorkflowRun","hostRef","row","collection","add","actorId","actorEmail","searchTokens","target","createdAt","serverTimestamp","catch","undefined","workflowRunRow","execution","input","failed","errors","length","action","event","join","slice","ending","result","trigger","outcomes","status","durationMs","type","id","workflowId","name","workflowName","runEventWorkflows","hostId","payload","depth","alerts","firestore","app","doc","triggered","where","limit","get","workflows","docs","filter","allWorkflowDocs","workflowMap","data","deletedAt","withId","$id","monthKey","Date","toISOString","runCounterRef","owner","org","counterSnapshot","workflowRunsPerMonth","used","Number","functionDocs","variableDocs","Promise","all","functions","definition","variables","variable","executed","workflow","trim","scope","startedAt","now","context","env","loadWorkflowContext","kind","run","ok","error","String","set","increment","merge","console","resumeWorkflowEnrollment","enrollment","ref","options","nowMs","actionId","exists","timedOut","startIndex","nextStepIndex","steps","enrollmentRef"],"mappings":"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"}
@@ -15,7 +15,7 @@
15
15
  * limitations under the License.
16
16
  */
17
17
  import { type HostActionStep, type HostActionStepType } from '@aglyn/aglyn/app-utils/actions';
18
- import { type HostWorkflow, type HostWorkflowStep } from '@aglyn/aglyn/app-utils/workflows';
18
+ import { type HostWorkflow, type HostWorkflowStep } from '../model/workflows';
19
19
  /**
20
20
  * ONE STEP MODEL FOR BOTH ENGINES.
21
21
  *
@@ -15,7 +15,7 @@ import { _ as _extends } from "@swc/helpers/_/_extends";
15
15
  * See the License for the specific language governing permissions and
16
16
  * limitations under the License.
17
17
  */ import { HOST_ACTION_STEP_LABELS, isClientActionStep, validateHostAction } from "@aglyn/aglyn/app-utils/actions";
18
- import { WORKFLOW_MAX_STEPS } from "@aglyn/aglyn/app-utils/workflows";
18
+ import { WORKFLOW_MAX_STEPS } from "../model/workflows.js";
19
19
  /** Whether a stored step is an Actions step rather than a function call. */ export function isWorkflowActionStep(step) {
20
20
  const type = step == null ? void 0 : step.type;
21
21
  return typeof type === 'string' && Object.prototype.hasOwnProperty.call(HOST_ACTION_STEP_LABELS, type);
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/plugins/workflows/src/lib/engine/workflow-steps.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\nimport {\n HOST_ACTION_STEP_LABELS,\n type HostActionStep,\n type HostActionStepType,\n isClientActionStep,\n validateHostAction,\n} from '@aglyn/aglyn/app-utils/actions'\nimport {\n type HostWorkflow,\n type HostWorkflowStep,\n WORKFLOW_MAX_STEPS,\n} from '@aglyn/aglyn/app-utils/workflows'\n\n/**\n * ONE STEP MODEL FOR BOTH ENGINES.\n *\n * A workflow step is one of two things:\n *\n * - a FUNCTION CALL — `{ functionId, functionName, args, resultName }`, the\n * shape every workflow has stored since the builder shipped, and still the\n * default. It evaluates its argument expressions and binds the result for\n * the steps after it;\n * - an ACTIONS STEP — `{ type, …fields, when? }`, exactly as the Actions\n * editor stores it: write a dataset record, send an email, update a CRM\n * contact, post a webhook, wait. It runs through the Actions executors,\n * with their validation, their tier gates and their run-history phrases.\n *\n * The two are told apart by `type`. An Actions step always carries one of the\n * Actions step types and a function call never has one, so a stored workflow\n * is read as it was written and nothing is migrated. A step whose `type` the\n * Actions vocabulary does not know is read as a function call, which is how\n * every workflow read it before — it fails as a call to no function.\n *\n * Pure and client-safe: the console's editor and the server's engine read the\n * same definitions from here.\n */\n\n/** A workflow step that runs one of the Actions steps, stored in the Actions shape. */\nexport type WorkflowActionStep = HostActionStep\n\n/** Any step a stored workflow holds. */\nexport type WorkflowStep = HostWorkflowStep | WorkflowActionStep\n\n/** A stored workflow, read with the unified step model. */\nexport interface AutomationWorkflow extends Omit<HostWorkflow, 'steps'> {\n steps: WorkflowStep[]\n}\n\n/** Whether a stored step is an Actions step rather than a function call. */\nexport function isWorkflowActionStep(step: unknown): step is WorkflowActionStep {\n const type = (step as { type?: unknown } | null | undefined)?.type\n return (\n typeof type === 'string' &&\n Object.prototype.hasOwnProperty.call(HOST_ACTION_STEP_LABELS, type)\n )\n}\n\n/** Whether a stored step is a function call. */\nexport function isWorkflowFunctionStep(step: unknown): step is HostWorkflowStep {\n return Boolean(step) && typeof step === 'object' && !isWorkflowActionStep(step)\n}\n\n/** Whether any step of a workflow is an Actions step. */\nexport function workflowHasActionSteps(\n workflow: { steps?: readonly unknown[] | null } | null | undefined,\n): boolean {\n return (workflow?.steps ?? []).some(isWorkflowActionStep)\n}\n\n/**\n * The Actions steps a workflow may hold: every step the SERVER runs.\n *\n * The executor's own rule, stated once. It runs every step that is not a\n * client step, and `siteAlert`, whose alert it hands back to the request that\n * raised the event. What is left out is what only a visitor's browser can do —\n * class toggles, drawers, menus, overlays, scrolling, custom HTML and\n * JavaScript, analytics events, redirects — and a workflow runs on a server\n * event, where there is no page to do it on.\n */\nexport const WORKFLOW_ACTION_STEP_TYPES: readonly HostActionStepType[] = (\n Object.keys(HOST_ACTION_STEP_LABELS) as HostActionStepType[]\n).filter(\n (type) =>\n type === 'siteAlert' || !isClientActionStep({ type } as HostActionStep),\n)\n\nconst WORKFLOW_ACTION_STEP_TYPE_SET: ReadonlySet<string> = new Set(\n WORKFLOW_ACTION_STEP_TYPES,\n)\n\n/** Whether a workflow may run an Actions step of this type. */\nexport function isWorkflowActionStepType(type: unknown): boolean {\n return typeof type === 'string' && WORKFLOW_ACTION_STEP_TYPE_SET.has(type)\n}\n\n/** How a step type reads in a sentence: its picker label, or its raw type. */\nexport function workflowStepTypeLabel(type: string): string {\n return HOST_ACTION_STEP_LABELS[type as HostActionStepType] ?? type\n}\n\n/**\n * Why a workflow cannot hold this Actions step, or null when it can.\n *\n * Shared by the editor, which refuses to save the step, and by the engine,\n * which refuses to run one a writer stored anyway — an import, the API, an\n * older client.\n */\nexport function workflowActionStepRefusal(step: WorkflowActionStep): string | null {\n if (isWorkflowActionStepType(step.type)) return null\n if (HOST_ACTION_STEP_LABELS[step.type as HostActionStepType]) {\n return (\n `“${workflowStepTypeLabel(step.type)}” runs in the visitor’s browser, ` +\n 'so a workflow cannot run it — build it as an interaction in Actions'\n )\n }\n return `“${String(step.type)}” is not a step a workflow can run`\n}\n\n/**\n * A workflow's steps checked the way the editors check them; null when they\n * pass, or the first problem, numbered by the step's place in the workflow.\n *\n * An Actions step is held to the Actions editor's own validator,\n * `validateHostAction`, one step at a time — the rules are that validator's,\n * never a second copy of them. The list is capped at `WORKFLOW_MAX_STEPS`,\n * the workflow's own ceiling rather than the ten an action is capped at.\n *\n * A FUNCTION CALL is not checked, deliberately. A workflow has always been\n * savable with a blank step — it is how the builder opens, and how an author\n * leaves a pipeline half-built and comes back to it — and a call naming no\n * function fails at run time with the evaluator's own words. Checking it here\n * would refuse a save that has been allowed since the builder shipped.\n */\nexport function validateWorkflowSteps(\n steps: readonly unknown[] | null | undefined,\n): string | null {\n const list = Array.isArray(steps) ? steps : []\n if (list.length > WORKFLOW_MAX_STEPS) {\n return `Workflows are capped at ${WORKFLOW_MAX_STEPS} steps`\n }\n for (const [index, step] of list.entries()) {\n const label = `Step ${index + 1}`\n if (!isWorkflowActionStep(step)) continue\n const refusal = workflowActionStepRefusal(step)\n if (refusal) return `${label}: ${refusal}`\n const problem = validateHostAction({\n name: 'workflow step',\n trigger: { event: 'formSubmission' },\n steps: [step],\n })\n if (problem) return problem.replace(/^Step 1\\b/, label)\n }\n return null\n}\n\n/**\n * The function calls of a workflow, as a workflow of their own, for the pure\n * evaluator — the console's test run, and anything else that evaluates a\n * workflow without performing it.\n *\n * Each call keeps the result name it would have had in place: an unnamed step\n * binds `step<N>` by its position, and the position is the one it holds among\n * ALL the steps, so a call after an Actions step keeps the name the steps\n * after it refer to.\n */\nexport function workflowFunctionCalls(\n workflow: AutomationWorkflow | HostWorkflow,\n): HostWorkflow {\n const steps: HostWorkflowStep[] = []\n ;(workflow.steps ?? []).forEach((step, index) => {\n if (!isWorkflowFunctionStep(step)) return\n steps.push({\n ...step,\n resultName: step.resultName?.trim() || `step${index + 1}`,\n })\n })\n return { ...workflow, steps } as HostWorkflow\n}\n"],"names":["HOST_ACTION_STEP_LABELS","isClientActionStep","validateHostAction","WORKFLOW_MAX_STEPS","isWorkflowActionStep","step","type","Object","prototype","hasOwnProperty","call","isWorkflowFunctionStep","Boolean","workflowHasActionSteps","workflow","steps","some","WORKFLOW_ACTION_STEP_TYPES","keys","filter","WORKFLOW_ACTION_STEP_TYPE_SET","Set","isWorkflowActionStepType","has","workflowStepTypeLabel","workflowActionStepRefusal","String","validateWorkflowSteps","list","Array","isArray","length","index","entries","label","refusal","problem","name","trigger","event","replace","workflowFunctionCalls","forEach","push","resultName","trim"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/workflows/src/lib/engine/workflow-steps.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\nimport {\n HOST_ACTION_STEP_LABELS,\n type HostActionStep,\n type HostActionStepType,\n isClientActionStep,\n validateHostAction,\n} from '@aglyn/aglyn/app-utils/actions'\nimport {\n type HostWorkflow,\n type HostWorkflowStep,\n WORKFLOW_MAX_STEPS,\n} from '../model/workflows'\n\n/**\n * ONE STEP MODEL FOR BOTH ENGINES.\n *\n * A workflow step is one of two things:\n *\n * - a FUNCTION CALL — `{ functionId, functionName, args, resultName }`, the\n * shape every workflow has stored since the builder shipped, and still the\n * default. It evaluates its argument expressions and binds the result for\n * the steps after it;\n * - an ACTIONS STEP — `{ type, …fields, when? }`, exactly as the Actions\n * editor stores it: write a dataset record, send an email, update a CRM\n * contact, post a webhook, wait. It runs through the Actions executors,\n * with their validation, their tier gates and their run-history phrases.\n *\n * The two are told apart by `type`. An Actions step always carries one of the\n * Actions step types and a function call never has one, so a stored workflow\n * is read as it was written and nothing is migrated. A step whose `type` the\n * Actions vocabulary does not know is read as a function call, which is how\n * every workflow read it before — it fails as a call to no function.\n *\n * Pure and client-safe: the console's editor and the server's engine read the\n * same definitions from here.\n */\n\n/** A workflow step that runs one of the Actions steps, stored in the Actions shape. */\nexport type WorkflowActionStep = HostActionStep\n\n/** Any step a stored workflow holds. */\nexport type WorkflowStep = HostWorkflowStep | WorkflowActionStep\n\n/** A stored workflow, read with the unified step model. */\nexport interface AutomationWorkflow extends Omit<HostWorkflow, 'steps'> {\n steps: WorkflowStep[]\n}\n\n/** Whether a stored step is an Actions step rather than a function call. */\nexport function isWorkflowActionStep(step: unknown): step is WorkflowActionStep {\n const type = (step as { type?: unknown } | null | undefined)?.type\n return (\n typeof type === 'string' &&\n Object.prototype.hasOwnProperty.call(HOST_ACTION_STEP_LABELS, type)\n )\n}\n\n/** Whether a stored step is a function call. */\nexport function isWorkflowFunctionStep(step: unknown): step is HostWorkflowStep {\n return Boolean(step) && typeof step === 'object' && !isWorkflowActionStep(step)\n}\n\n/** Whether any step of a workflow is an Actions step. */\nexport function workflowHasActionSteps(\n workflow: { steps?: readonly unknown[] | null } | null | undefined,\n): boolean {\n return (workflow?.steps ?? []).some(isWorkflowActionStep)\n}\n\n/**\n * The Actions steps a workflow may hold: every step the SERVER runs.\n *\n * The executor's own rule, stated once. It runs every step that is not a\n * client step, and `siteAlert`, whose alert it hands back to the request that\n * raised the event. What is left out is what only a visitor's browser can do —\n * class toggles, drawers, menus, overlays, scrolling, custom HTML and\n * JavaScript, analytics events, redirects — and a workflow runs on a server\n * event, where there is no page to do it on.\n */\nexport const WORKFLOW_ACTION_STEP_TYPES: readonly HostActionStepType[] = (\n Object.keys(HOST_ACTION_STEP_LABELS) as HostActionStepType[]\n).filter(\n (type) =>\n type === 'siteAlert' || !isClientActionStep({ type } as HostActionStep),\n)\n\nconst WORKFLOW_ACTION_STEP_TYPE_SET: ReadonlySet<string> = new Set(\n WORKFLOW_ACTION_STEP_TYPES,\n)\n\n/** Whether a workflow may run an Actions step of this type. */\nexport function isWorkflowActionStepType(type: unknown): boolean {\n return typeof type === 'string' && WORKFLOW_ACTION_STEP_TYPE_SET.has(type)\n}\n\n/** How a step type reads in a sentence: its picker label, or its raw type. */\nexport function workflowStepTypeLabel(type: string): string {\n return HOST_ACTION_STEP_LABELS[type as HostActionStepType] ?? type\n}\n\n/**\n * Why a workflow cannot hold this Actions step, or null when it can.\n *\n * Shared by the editor, which refuses to save the step, and by the engine,\n * which refuses to run one a writer stored anyway — an import, the API, an\n * older client.\n */\nexport function workflowActionStepRefusal(step: WorkflowActionStep): string | null {\n if (isWorkflowActionStepType(step.type)) return null\n if (HOST_ACTION_STEP_LABELS[step.type as HostActionStepType]) {\n return (\n `“${workflowStepTypeLabel(step.type)}” runs in the visitor’s browser, ` +\n 'so a workflow cannot run it — build it as an interaction in Actions'\n )\n }\n return `“${String(step.type)}” is not a step a workflow can run`\n}\n\n/**\n * A workflow's steps checked the way the editors check them; null when they\n * pass, or the first problem, numbered by the step's place in the workflow.\n *\n * An Actions step is held to the Actions editor's own validator,\n * `validateHostAction`, one step at a time — the rules are that validator's,\n * never a second copy of them. The list is capped at `WORKFLOW_MAX_STEPS`,\n * the workflow's own ceiling rather than the ten an action is capped at.\n *\n * A FUNCTION CALL is not checked, deliberately. A workflow has always been\n * savable with a blank step — it is how the builder opens, and how an author\n * leaves a pipeline half-built and comes back to it — and a call naming no\n * function fails at run time with the evaluator's own words. Checking it here\n * would refuse a save that has been allowed since the builder shipped.\n */\nexport function validateWorkflowSteps(\n steps: readonly unknown[] | null | undefined,\n): string | null {\n const list = Array.isArray(steps) ? steps : []\n if (list.length > WORKFLOW_MAX_STEPS) {\n return `Workflows are capped at ${WORKFLOW_MAX_STEPS} steps`\n }\n for (const [index, step] of list.entries()) {\n const label = `Step ${index + 1}`\n if (!isWorkflowActionStep(step)) continue\n const refusal = workflowActionStepRefusal(step)\n if (refusal) return `${label}: ${refusal}`\n const problem = validateHostAction({\n name: 'workflow step',\n trigger: { event: 'formSubmission' },\n steps: [step],\n })\n if (problem) return problem.replace(/^Step 1\\b/, label)\n }\n return null\n}\n\n/**\n * The function calls of a workflow, as a workflow of their own, for the pure\n * evaluator — the console's test run, and anything else that evaluates a\n * workflow without performing it.\n *\n * Each call keeps the result name it would have had in place: an unnamed step\n * binds `step<N>` by its position, and the position is the one it holds among\n * ALL the steps, so a call after an Actions step keeps the name the steps\n * after it refer to.\n */\nexport function workflowFunctionCalls(\n workflow: AutomationWorkflow | HostWorkflow,\n): HostWorkflow {\n const steps: HostWorkflowStep[] = []\n ;(workflow.steps ?? []).forEach((step, index) => {\n if (!isWorkflowFunctionStep(step)) return\n steps.push({\n ...step,\n resultName: step.resultName?.trim() || `step${index + 1}`,\n })\n })\n return { ...workflow, steps } as HostWorkflow\n}\n"],"names":["HOST_ACTION_STEP_LABELS","isClientActionStep","validateHostAction","WORKFLOW_MAX_STEPS","isWorkflowActionStep","step","type","Object","prototype","hasOwnProperty","call","isWorkflowFunctionStep","Boolean","workflowHasActionSteps","workflow","steps","some","WORKFLOW_ACTION_STEP_TYPES","keys","filter","WORKFLOW_ACTION_STEP_TYPE_SET","Set","isWorkflowActionStepType","has","workflowStepTypeLabel","workflowActionStepRefusal","String","validateWorkflowSteps","list","Array","isArray","length","index","entries","label","refusal","problem","name","trigger","event","replace","workflowFunctionCalls","forEach","push","resultName","trim"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/plugins/workflows/src/lib/model/org-automations.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\nimport {\n ACTION_MAX_CONDITIONS,\n ACTION_MAX_STEPS,\n HOST_ACTION_STEP_LABELS,\n type HostActionStep,\n type HostActionStepType,\n type HostActionTriggerCondition,\n TRIGGER_COMBINATORS,\n TRIGGER_CONDITION_OPS,\n type TriggerCombinator,\n validateHostAction,\n} from '@aglyn/aglyn/app-utils/actions'\nimport {\n normalizeVisibleTo,\n type ScopeToken,\n visibleToHost,\n} from '@aglyn/aglyn/app-utils/scope-tokens'\nimport type { HostEventType } from '@aglyn/aglyn/app-utils/workflows'\n\n/**\n * ORG AUTOMATIONS (AGL-3302): one automation the organization writes once and\n * places on the sites it chooses.\n *\n * A site's own actions stay where they are, at `hosts/{hostId}/actions`. They\n * are not moved here, and the reasons are the four things a site action can\n * do that an org automation must not:\n *\n * - call the site's functions and variables through a workflow step — the\n * working set a computed variable binds to, which is one site's;\n * - run the steps only a visitor's page can run (overlays, drawers, class\n * toggles, a redirect to a screen), which the `actions` collection also\n * holds for element interactions;\n * - be fired by `/api/events/dispatch`, which is unauthenticated and ties an\n * action id to a site only by its path;\n * - wait, with the resume beat reading a missing document as the kill\n * switch, so a migration would stop every person already inside a flow.\n *\n * An org automation is therefore its own document with a narrower\n * vocabulary: triggers only the server raises, and steps that mean the same\n * thing on whichever site they run. Each run executes AS the site the event\n * happened on — that site's sender, suppressions, consent group, meters and\n * run allowance — so placing one automation on five sites is five sites each\n * running it for themselves, never one site acting for the others.\n *\n * Pure and client-safe: the console's editor, the server routes and the\n * engine read the same definitions from here.\n */\n\n/** `orgs/{orgId}/automations/{automationId}`. */\nexport const ORG_AUTOMATIONS_COLLECTION = 'automations'\n\n/**\n * The two server doors onto the collection, under the plugin's `automations`\n * API prefix: the organization's (`manage`) and a site's own pause\n * (`pause`). Named here so the console's caller and the server's registration\n * read one spelling.\n */\nexport const ORG_AUTOMATION_API_ROUTES = {\n manage: 'automations/manage',\n pause: 'automations/pause',\n} as const\n\n/**\n * How many live org automations one organization may hold.\n *\n * A flat platform cap in the shape `ACTIONS_MAX_PER_HOST` takes, and smaller,\n * because the two are different things: a site action is authored per site\n * and per element, where an org automation is a shared building block placed\n * on many sites at once. A hundred is far past what an organization writes by\n * hand and keeps the org hub's list — which reads every live one — a single\n * bounded window. Soft-deleted documents do not count.\n */\nexport const ORG_AUTOMATIONS_MAX = 100\n\n/**\n * How many org automations one event may run on one site: the same fan-out\n * bound the site's own actions take (`MAX_TRIGGERED_ACTIONS`), counted\n * separately so the two cannot crowd each other out of the query.\n */\nexport const MAX_TRIGGERED_ORG_AUTOMATIONS = 10\n\n/**\n * The run history's target type for a run of an org automation, on the\n * activity feed of the site it ran on.\n */\nexport const ORG_AUTOMATION_RUN_TARGET = 'orgAutomation'\n\n/**\n * The org activity target an org automation's own lifecycle rows are filed\n * under: the plugin's namespaced resource (`pluginId:noun`), which the org's\n * feed reads as \"Automation\" — core's target list names only core's resources.\n */\nexport const ORG_AUTOMATION_ACTIVITY_TARGET = 'workflows:automation'\n\n/**\n * The events an org automation may start on: every host event the SERVER\n * raises and a person can be the subject of.\n *\n * Listed rather than derived from `HOST_EVENT_TYPES`, deliberately. The engine\n * queries the organization's automations on every event in this list, so a\n * new host event must not become an org trigger — and a new read on every\n * occurrence of it — by being added to the platform's list. Two are left out\n * on purpose: `pageView` fires on every request to every published page, and\n * an org query on it would be a read per visitor; `memberSignOut` is raised\n * by no door today.\n */\nexport const ORG_AUTOMATION_TRIGGER_EVENTS = [\n 'formSubmission',\n 'lead',\n 'contactCreated',\n 'contactStageChanged',\n 'booking',\n 'memberSignUp',\n 'memberSignIn',\n 'dealStageChanged',\n 'dealWon',\n 'dealLost',\n 'taskCompleted',\n] as const satisfies readonly HostEventType[]\n\nexport type OrgAutomationTriggerEvent =\n (typeof ORG_AUTOMATION_TRIGGER_EVENTS)[number]\n\nconst ORG_TRIGGER_SET: ReadonlySet<string> = new Set(\n ORG_AUTOMATION_TRIGGER_EVENTS,\n)\n\n/** Whether an org automation may start on this event. */\nexport function isOrgAutomationTriggerEvent(\n event: unknown,\n): event is OrgAutomationTriggerEvent {\n return typeof event === 'string' && ORG_TRIGGER_SET.has(event)\n}\n\n/**\n * The steps an org automation may hold: the server steps that read the same\n * on every site.\n *\n * What is left out, and why:\n * - every step the visitor's page runs — overlays, drawers, menus, class\n * toggles, custom HTML and JavaScript, analytics, redirects, the site\n * alert — because an org trigger is a server event with no page to act on;\n * - `runWorkflow`, because a workflow is one site's and calls that site's\n * functions and variables;\n * - `webhookPost`, because a webhook's address and secret are one site's.\n *\n * `customEvent` stays: it fires a custom event ON the site the run is on,\n * which that site's own actions may listen for — the documented way for an\n * org automation to hand over to something site-specific.\n */\nexport const ORG_AUTOMATION_STEP_TYPES = [\n 'sendEmail',\n 'notifyAdmins',\n 'enrollList',\n 'assignCampaign',\n 'datasetAppend',\n 'updateDataset',\n 'setContactStage',\n 'addContactTag',\n 'assignContactOwner',\n 'createCrmTask',\n 'logCrmActivity',\n 'customEvent',\n 'wait',\n 'waitForEvent',\n 'exitFlow',\n] as const satisfies readonly HostActionStepType[]\n\nexport type OrgAutomationStepType = (typeof ORG_AUTOMATION_STEP_TYPES)[number]\n\nconst ORG_STEP_SET: ReadonlySet<string> = new Set(ORG_AUTOMATION_STEP_TYPES)\n\n/** Whether an org automation may run a step of this type. */\nexport function isOrgAutomationStepType(\n type: unknown,\n): type is OrgAutomationStepType {\n return typeof type === 'string' && ORG_STEP_SET.has(type)\n}\n\n/** The org editor's \"Do\" picker: each allowed step with its label, in order. */\nexport const ORG_AUTOMATION_STEP_KINDS: ReadonlyArray<{\n value: OrgAutomationStepType\n label: string\n}> = ORG_AUTOMATION_STEP_TYPES.map((value) => ({\n value,\n label: HOST_ACTION_STEP_LABELS[value],\n}))\n\n/**\n * Why an org automation cannot hold this step, or null when it can.\n *\n * Shared by the save route, which refuses it, and by the engine, which\n * refuses to run one a writer stored anyway.\n */\nexport function orgAutomationStepRefusal(step: {\n type?: unknown\n}): string | null {\n if (isOrgAutomationStepType(step?.type)) return null\n const label = HOST_ACTION_STEP_LABELS[step?.type as HostActionStepType]\n if (!label) return `“${String(step?.type ?? '')}” is not a step`\n return (\n `“${label}” belongs to one site, so an org automation cannot run it — ` +\n 'build it as an action on that site'\n )\n}\n\n/** The trigger an org automation starts on, as it is stored. */\nexport interface OrgAutomationTrigger {\n event: OrgAutomationTriggerEvent\n /** Optional expression over the payload; runs only when truthy. */\n filter?: string\n conditions: HostActionTriggerCondition[] | null\n combinator: TriggerCombinator | null\n}\n\n/**\n * The fields an org automation is written with — by the save route, and by\n * nothing else: the collection is closed to client writes.\n */\nexport interface OrgAutomationFields {\n name: string\n trigger: OrgAutomationTrigger\n steps: HostActionStep[]\n enabled: boolean\n /**\n * The sites it runs on, as scope tokens: `['org']` is every site of the\n * organization, now and later; `['host:{id}', …]` names up to thirty.\n */\n visibleTo: ScopeToken[]\n}\n\n/** `orgs/{orgId}/automations/{id}`, as it is stored. */\nexport interface OrgAutomation extends OrgAutomationFields {\n /**\n * The sites that have paused it for themselves — each a site id, never a\n * scope token. The \"host level control\": a site's admin or editor adds or\n * removes their own site here, and only their own.\n */\n pausedHostIds: string[]\n createdBy?: string\n updatedBy?: string\n /**\n * Set when it was deleted. Always written — `null` while live — so the\n * org hub's list can ask for the live ones by equality.\n */\n deletedAt: unknown\n}\n\n/** A stored org automation with its id, as the console lists it. */\nexport type OrgAutomationRow = OrgAutomation & { $id: string }\n\n/** Longest name an org automation may carry: the actions editor's cap. */\nexport const ORG_AUTOMATION_NAME_MAX = 60\n\n/** Longest trigger filter expression. */\nconst FILTER_MAX = 500\n\n/** Longest a condition's field or value may be. */\nconst CONDITION_FIELD_MAX = 64\nconst CONDITION_VALUE_MAX = 200\n\n/**\n * Every field a step of each type may carry beyond `type` and `when`, with\n * the longest a string in it may be. What the save route stores is exactly\n * this: a key a step's type does not list is dropped, so the document holds\n * the vocabulary and nothing a caller made up.\n *\n * The caps follow what the engine reads — it cuts a subject at 200 and a body\n * at 5,000 before sending — so nothing is stored that could not be run.\n */\nconst STEP_FIELDS: Record<\n OrgAutomationStepType,\n Readonly<Record<string, 'string' | 'number' | 'boolean' | number>>\n> = {\n sendEmail: { subject: 200, body: 5000, toField: 64, topicId: 128 },\n notifyAdmins: { title: 200, body: 500 },\n enrollList: { listId: 128, listName: 200 },\n assignCampaign: { campaignId: 128, campaignName: 200 },\n datasetAppend: { datasetId: 128, datasetName: 200 },\n updateDataset: { datasetId: 128, datasetName: 200 },\n setContactStage: { lifecycleStage: 64 },\n addContactTag: { tag: 200 },\n assignContactOwner: {\n ownerUid: 128,\n ownerEmail: 320,\n roundRobin: 'boolean',\n },\n createCrmTask: {\n title: 200,\n kind: 64,\n dueInDays: 'number',\n assigneeUid: 128,\n assigneeEmail: 320,\n },\n logCrmActivity: { kind: 64, body: 2000 },\n customEvent: { eventName: 64 },\n wait: { delayMinutes: 'number' },\n waitForEvent: { eventName: 64, timeoutMinutes: 'number' },\n exitFlow: {},\n}\n\nconst asObject = (value: unknown): Record<string, unknown> | null =>\n value && typeof value === 'object' && !Array.isArray(value)\n ? (value as Record<string, unknown>)\n : null\n\n/** A condition as it is stored, or null for a row that is not one. */\nfunction readCondition(raw: unknown): HostActionTriggerCondition | null {\n const row = asObject(raw)\n if (!row) return null\n const op = row['op']\n if (!(TRIGGER_CONDITION_OPS as readonly unknown[]).includes(op)) {\n // Kept as written so the validator names the missing operator rather\n // than the row silently vanishing.\n return {\n field: String(row['field'] ?? '').trim().slice(0, CONDITION_FIELD_MAX),\n op: op as never,\n }\n }\n return {\n field: String(row['field'] ?? '').trim().slice(0, CONDITION_FIELD_MAX),\n op: op as HostActionTriggerCondition['op'],\n ...(op !== 'notEmpty'\n ? { value: String(row['value'] ?? '').trim().slice(0, CONDITION_VALUE_MAX) }\n : {}),\n }\n}\n\nfunction readConditions(raw: unknown): HostActionTriggerCondition[] {\n return (Array.isArray(raw) ? raw : [])\n .slice(0, ACTION_MAX_CONDITIONS + 1)\n .map(readCondition)\n .filter((row): row is HostActionTriggerCondition => row !== null)\n}\n\nfunction readCombinator(raw: unknown): TriggerCombinator | null {\n return (TRIGGER_COMBINATORS as readonly unknown[]).includes(raw)\n ? (raw as TriggerCombinator)\n : null\n}\n\n/** One step as it would be stored: its own fields only, strings capped. */\nfunction readStep(raw: unknown): HostActionStep | { type: unknown } {\n const source = asObject(raw) ?? {}\n const type = source['type']\n if (!isOrgAutomationStepType(type)) return { type }\n const step: Record<string, unknown> = { type }\n for (const [key, kind] of Object.entries(STEP_FIELDS[type])) {\n const value = source[key]\n if (value === undefined || value === null) continue\n if (kind === 'boolean') {\n if (typeof value === 'boolean') step[key] = value\n } else if (kind === 'number') {\n if (typeof value === 'number' && Number.isFinite(value)) step[key] = value\n } else if (typeof value === 'string') {\n step[key] = value.slice(0, Number(kind))\n }\n }\n const when = asObject(source['when'])\n if (when) {\n const conditions = readConditions(when['conditions'])\n if (conditions.length) {\n step['when'] = {\n conditions,\n combinator: readCombinator(when['combinator']) ?? 'and',\n }\n }\n }\n return step as HostActionStep\n}\n\n/** What a caller sends to create or replace an org automation. */\nexport type OrgAutomationInput = Record<string, unknown>\n\n/**\n * Reads an org automation out of an untrusted request body: the fields it may\n * hold, in the shape they are stored, or the first reason it cannot be saved.\n *\n * The rules are the actions builder's own — `validateHostAction` checks the\n * name, the trigger's conditions and every step's fields — and on top of them\n * the two this vocabulary adds: the trigger must be an org trigger and every\n * step an org step. The placement must be a scope a save can store:\n * `normalizeVisibleTo` refuses an empty one and one over thirty sites rather\n * than guessing between sharing wider and taking access away.\n *\n * Whether each named site belongs to the organization is the route's to ask,\n * because it takes a read.\n */\nexport function readOrgAutomation(\n input: unknown,\n): { ok: true; value: OrgAutomationFields } | { ok: false; problem: string } {\n const body = asObject(input) ?? {}\n const name = String(body['name'] ?? '')\n .trim()\n .slice(0, ORG_AUTOMATION_NAME_MAX)\n if (!name) return { ok: false, problem: 'Name the automation' }\n const trigger = asObject(body['trigger']) ?? {}\n const event = String(trigger['event'] ?? '').trim()\n if (!isOrgAutomationTriggerEvent(event)) {\n return {\n ok: false,\n problem:\n 'Pick a trigger an org automation can start on — a form, a lead, a ' +\n 'booking, a member or a CRM event',\n }\n }\n const filter = String(trigger['filter'] ?? '')\n .trim()\n .slice(0, FILTER_MAX)\n const conditions = readConditions(trigger['conditions'])\n const combinator = readCombinator(trigger['combinator'])\n const rawSteps = Array.isArray(body['steps']) ? body['steps'] : []\n if (rawSteps.length > ACTION_MAX_STEPS) {\n return {\n ok: false,\n problem: `Org automations are capped at ${ACTION_MAX_STEPS} steps`,\n }\n }\n const steps = rawSteps.map(readStep)\n for (const [index, step] of steps.entries()) {\n const refusal = orgAutomationStepRefusal(step)\n if (refusal) return { ok: false, problem: `Step ${index + 1}: ${refusal}` }\n }\n const problem = validateHostAction({\n name,\n trigger: {\n event,\n ...(filter ? { filter } : {}),\n ...(conditions.length ? { conditions } : {}),\n ...(combinator ? { combinator } : {}),\n },\n steps: steps as HostActionStep[],\n })\n if (problem) return { ok: false, problem }\n const visibleTo = normalizeVisibleTo(\n Array.isArray(body['visibleTo'])\n ? (body['visibleTo'] as unknown[]).filter(\n (token): token is string => typeof token === 'string',\n )\n : [],\n )\n if (!visibleTo) {\n return {\n ok: false,\n problem: 'Choose every site, or up to 30 sites, for it to run on',\n }\n }\n return {\n ok: true,\n value: {\n name,\n trigger: {\n event,\n ...(filter ? { filter } : {}),\n conditions: conditions.length ? conditions : null,\n combinator: conditions.length ? (combinator ?? 'and') : null,\n },\n steps: steps as HostActionStep[],\n enabled: body['enabled'] !== false,\n visibleTo,\n },\n }\n}\n\n/** A stored document's pause list, read defensively. */\nexport function orgAutomationPausedHostIds(\n automation: { pausedHostIds?: unknown } | null | undefined,\n): string[] {\n const paused = automation?.pausedHostIds\n return Array.isArray(paused)\n ? paused.filter((id): id is string => typeof id === 'string')\n : []\n}\n\n/**\n * Why a stored org automation will not run on this site, or null when it\n * will — the one answer the engine asks when an event arrives and again when\n * a person waiting inside the automation is due.\n *\n * Deleted, switched off, taken off the site and paused there are the four\n * ways an organization or a site says \"not here\", and every one of them\n * stops the people already waiting on this site as well as the next run: the\n * kill switch a site action's resume applies, for the same reason.\n */\nexport function orgAutomationStopReason(\n automation:\n | {\n deletedAt?: unknown\n enabled?: unknown\n visibleTo?: unknown\n pausedHostIds?: unknown\n }\n | null\n | undefined,\n hostId: string,\n): string | null {\n if (!automation || automation.deletedAt) {\n return 'the org automation was deleted'\n }\n if (automation.enabled === false) {\n return 'the org automation was switched off'\n }\n if (!visibleToHost(automation.visibleTo as string[] | undefined, hostId)) {\n return 'the org automation no longer runs on this site'\n }\n if (orgAutomationPausedHostIds(automation).includes(hostId)) {\n return 'the org automation is paused on this site'\n }\n return null\n}\n\n/** Whether a stored org automation runs on this site. */\nexport function orgAutomationRunsOnHost(\n automation: Parameters<typeof orgAutomationStopReason>[0],\n hostId: string,\n): boolean {\n return orgAutomationStopReason(automation, hostId) === null\n}\n\n/**\n * A pause list after the placement changed: a site the automation no longer\n * runs on has nothing left to pause, so its entry goes with it, and a site\n * placed on it again later starts unpaused.\n */\nexport function prunePausedHostIds(\n pausedHostIds: readonly string[],\n visibleTo: readonly string[],\n): string[] {\n return pausedHostIds.filter((hostId) => visibleToHost(visibleTo, hostId))\n}\n"],"names":["ACTION_MAX_CONDITIONS","ACTION_MAX_STEPS","HOST_ACTION_STEP_LABELS","TRIGGER_COMBINATORS","TRIGGER_CONDITION_OPS","validateHostAction","normalizeVisibleTo","visibleToHost","ORG_AUTOMATIONS_COLLECTION","ORG_AUTOMATION_API_ROUTES","manage","pause","ORG_AUTOMATIONS_MAX","MAX_TRIGGERED_ORG_AUTOMATIONS","ORG_AUTOMATION_RUN_TARGET","ORG_AUTOMATION_ACTIVITY_TARGET","ORG_AUTOMATION_TRIGGER_EVENTS","ORG_TRIGGER_SET","Set","isOrgAutomationTriggerEvent","event","has","ORG_AUTOMATION_STEP_TYPES","ORG_STEP_SET","isOrgAutomationStepType","type","ORG_AUTOMATION_STEP_KINDS","map","value","label","orgAutomationStepRefusal","step","String","ORG_AUTOMATION_NAME_MAX","FILTER_MAX","CONDITION_FIELD_MAX","CONDITION_VALUE_MAX","STEP_FIELDS","sendEmail","subject","body","toField","topicId","notifyAdmins","title","enrollList","listId","listName","assignCampaign","campaignId","campaignName","datasetAppend","datasetId","datasetName","updateDataset","setContactStage","lifecycleStage","addContactTag","tag","assignContactOwner","ownerUid","ownerEmail","roundRobin","createCrmTask","kind","dueInDays","assigneeUid","assigneeEmail","logCrmActivity","customEvent","eventName","wait","delayMinutes","waitForEvent","timeoutMinutes","exitFlow","asObject","Array","isArray","readCondition","raw","row","op","includes","field","trim","slice","readConditions","filter","readCombinator","readStep","source","key","Object","entries","undefined","Number","isFinite","when","conditions","length","combinator","readOrgAutomation","input","trigger","name","ok","problem","rawSteps","steps","index","refusal","visibleTo","token","enabled","orgAutomationPausedHostIds","automation","paused","pausedHostIds","id","orgAutomationStopReason","hostId","deletedAt","orgAutomationRunsOnHost","prunePausedHostIds"],"mappings":";AAAA;;;;;;;;;;;;;;;CAeC,GAED,SACEA,qBAAqB,EACrBC,gBAAgB,EAChBC,uBAAuB,EAIvBC,mBAAmB,EACnBC,qBAAqB,EAErBC,kBAAkB,QACb,iCAAgC;AACvC,SACEC,kBAAkB,EAElBC,aAAa,QACR,sCAAqC;AAG5C;;;;;;;;;;;;;;;;;;;;;;;;;;;CA2BC,GAED,+CAA+C,GAC/C,OAAO,MAAMC,6BAA6B,cAAa;AAEvD;;;;;CAKC,GACD,OAAO,MAAMC,4BAA4B;IACvCC,QAAQ;IACRC,OAAO;AACT,EAAU;AAEV;;;;;;;;;CASC,GACD,OAAO,MAAMC,sBAAsB,IAAG;AAEtC;;;;CAIC,GACD,OAAO,MAAMC,gCAAgC,GAAE;AAE/C;;;CAGC,GACD,OAAO,MAAMC,4BAA4B,gBAAe;AAExD;;;;CAIC,GACD,OAAO,MAAMC,iCAAiC,uBAAsB;AAEpE;;;;;;;;;;;CAWC,GACD,OAAO,MAAMC,gCAAgC;IAC3C;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;CACD,CAA4C;AAK7C,MAAMC,kBAAuC,IAAIC,IAC/CF;AAGF,uDAAuD,GACvD,OAAO,SAASG,4BACdC,KAAc;IAEd,OAAO,OAAOA,UAAU,YAAYH,gBAAgBI,GAAG,CAACD;AAC1D;AAEA;;;;;;;;;;;;;;;CAeC,GACD,OAAO,MAAME,4BAA4B;IACvC;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;CACD,CAAiD;AAIlD,MAAMC,eAAoC,IAAIL,IAAII;AAElD,2DAA2D,GAC3D,OAAO,SAASE,wBACdC,IAAa;IAEb,OAAO,OAAOA,SAAS,YAAYF,aAAaF,GAAG,CAACI;AACtD;AAEA,8EAA8E,GAC9E,OAAO,MAAMC,4BAGRJ,0BAA0BK,GAAG,CAAC,CAACC,QAAW,CAAA;QAC7CA;QACAC,OAAO3B,uBAAuB,CAAC0B,MAAM;IACvC,CAAA,GAAG;AAEH;;;;;CAKC,GACD,OAAO,SAASE,yBAAyBC,IAExC;;IACC,IAAIP,wBAAwBO,wBAAAA,KAAMN,IAAI,GAAG,OAAO;IAChD,MAAMI,QAAQ3B,uBAAuB,CAAC6B,wBAAAA,KAAMN,IAAI,CAAuB;IACvE,IAAI,CAACI,OAAO,OAAO,CAAC,CAAC,EAAEG,eAAOD,wBAAAA,KAAMN,IAAI,mBAAI,IAAI,eAAe,CAAC;IAChE,OACE,CAAC,CAAC,EAAEI,MAAM,4DAA4D,CAAC,GACvE;AAEJ;AA+CA,wEAAwE,GACxE,OAAO,MAAMI,0BAA0B,GAAE;AAEzC,uCAAuC,GACvC,MAAMC,aAAa;AAEnB,iDAAiD,GACjD,MAAMC,sBAAsB;AAC5B,MAAMC,sBAAsB;AAE5B;;;;;;;;CAQC,GACD,MAAMC,cAGF;IACFC,WAAW;QAAEC,SAAS;QAAKC,MAAM;QAAMC,SAAS;QAAIC,SAAS;IAAI;IACjEC,cAAc;QAAEC,OAAO;QAAKJ,MAAM;IAAI;IACtCK,YAAY;QAAEC,QAAQ;QAAKC,UAAU;IAAI;IACzCC,gBAAgB;QAAEC,YAAY;QAAKC,cAAc;IAAI;IACrDC,eAAe;QAAEC,WAAW;QAAKC,aAAa;IAAI;IAClDC,eAAe;QAAEF,WAAW;QAAKC,aAAa;IAAI;IAClDE,iBAAiB;QAAEC,gBAAgB;IAAG;IACtCC,eAAe;QAAEC,KAAK;IAAI;IAC1BC,oBAAoB;QAClBC,UAAU;QACVC,YAAY;QACZC,YAAY;IACd;IACAC,eAAe;QACbnB,OAAO;QACPoB,MAAM;QACNC,WAAW;QACXC,aAAa;QACbC,eAAe;IACjB;IACAC,gBAAgB;QAAEJ,MAAM;QAAIxB,MAAM;IAAK;IACvC6B,aAAa;QAAEC,WAAW;IAAG;IAC7BC,MAAM;QAAEC,cAAc;IAAS;IAC/BC,cAAc;QAAEH,WAAW;QAAII,gBAAgB;IAAS;IACxDC,UAAU,CAAC;AACb;AAEA,MAAMC,WAAW,CAAChD,QAChBA,SAAS,OAAOA,UAAU,YAAY,CAACiD,MAAMC,OAAO,CAAClD,SAChDA,QACD;AAEN,oEAAoE,GACpE,SAASmD,cAAcC,GAAY;QAajBC,YAGMA;IAftB,MAAMA,MAAML,SAASI;IACrB,IAAI,CAACC,KAAK,OAAO;IACjB,MAAMC,KAAKD,GAAG,CAAC,KAAK;IACpB,IAAI,CAAC,AAAC7E,sBAA6C+E,QAAQ,CAACD,KAAK;YAI/CD;QAHhB,qEAAqE;QACrE,mCAAmC;QACnC,OAAO;YACLG,OAAOpD,QAAOiD,cAAAA,GAAG,CAAC,QAAQ,YAAZA,cAAgB,IAAII,IAAI,GAAGC,KAAK,CAAC,GAAGnD;YAClD+C,IAAIA;QACN;IACF;IACA,OAAO;QACLE,OAAOpD,QAAOiD,aAAAA,GAAG,CAAC,QAAQ,YAAZA,aAAgB,IAAII,IAAI,GAAGC,KAAK,CAAC,GAAGnD;QAClD+C,IAAIA;OACAA,OAAO,aACP;QAAEtD,OAAOI,QAAOiD,aAAAA,GAAG,CAAC,QAAQ,YAAZA,aAAgB,IAAII,IAAI,GAAGC,KAAK,CAAC,GAAGlD;IAAqB,IACzE,CAAC;AAET;AAEA,SAASmD,eAAeP,GAAY;IAClC,OAAO,AAACH,CAAAA,MAAMC,OAAO,CAACE,OAAOA,MAAM,EAAE,AAAD,EACjCM,KAAK,CAAC,GAAGtF,wBAAwB,GACjC2B,GAAG,CAACoD,eACJS,MAAM,CAAC,CAACP,MAA2CA,QAAQ;AAChE;AAEA,SAASQ,eAAeT,GAAY;IAClC,OAAO,AAAC7E,oBAA2CgF,QAAQ,CAACH,OACvDA,MACD;AACN;AAEA,yEAAyE,GACzE,SAASU,SAASV,GAAY;QACbJ;IAAf,MAAMe,UAASf,YAAAA,SAASI,gBAATJ,YAAiB,CAAC;IACjC,MAAMnD,OAAOkE,MAAM,CAAC,OAAO;IAC3B,IAAI,CAACnE,wBAAwBC,OAAO,OAAO;QAAEA;IAAK;IAClD,MAAMM,OAAgC;QAAEN;IAAK;IAC7C,KAAK,MAAM,CAACmE,KAAK5B,KAAK,IAAI6B,OAAOC,OAAO,CAACzD,WAAW,CAACZ,KAAK,EAAG;QAC3D,MAAMG,QAAQ+D,MAAM,CAACC,IAAI;QACzB,IAAIhE,UAAUmE,aAAanE,UAAU,MAAM;QAC3C,IAAIoC,SAAS,WAAW;YACtB,IAAI,OAAOpC,UAAU,WAAWG,IAAI,CAAC6D,IAAI,GAAGhE;QAC9C,OAAO,IAAIoC,SAAS,UAAU;YAC5B,IAAI,OAAOpC,UAAU,YAAYoE,OAAOC,QAAQ,CAACrE,QAAQG,IAAI,CAAC6D,IAAI,GAAGhE;QACvE,OAAO,IAAI,OAAOA,UAAU,UAAU;YACpCG,IAAI,CAAC6D,IAAI,GAAGhE,MAAM0D,KAAK,CAAC,GAAGU,OAAOhC;QACpC;IACF;IACA,MAAMkC,OAAOtB,SAASe,MAAM,CAAC,OAAO;IACpC,IAAIO,MAAM;QACR,MAAMC,aAAaZ,eAAeW,IAAI,CAAC,aAAa;QACpD,IAAIC,WAAWC,MAAM,EAAE;gBAGPX;YAFd1D,IAAI,CAAC,OAAO,GAAG;gBACboE;gBACAE,UAAU,GAAEZ,kBAAAA,eAAeS,IAAI,CAAC,aAAa,aAAjCT,kBAAsC;YACpD;QACF;IACF;IACA,OAAO1D;AACT;AAKA;;;;;;;;;;;;;CAaC,GACD,OAAO,SAASuE,kBACdC,KAAc;QAED3B,WACOpC,YAIJoC,YACK4B,gBASCA;IAftB,MAAMhE,QAAOoC,YAAAA,SAAS2B,kBAAT3B,YAAmB,CAAC;IACjC,MAAM6B,OAAOzE,QAAOQ,aAAAA,IAAI,CAAC,OAAO,YAAZA,aAAgB,IACjC6C,IAAI,GACJC,KAAK,CAAC,GAAGrD;IACZ,IAAI,CAACwE,MAAM,OAAO;QAAEC,IAAI;QAAOC,SAAS;IAAsB;IAC9D,MAAMH,WAAU5B,aAAAA,SAASpC,IAAI,CAAC,UAAU,aAAxBoC,aAA6B,CAAC;IAC9C,MAAMxD,QAAQY,QAAOwE,iBAAAA,OAAO,CAAC,QAAQ,YAAhBA,iBAAoB,IAAInB,IAAI;IACjD,IAAI,CAAClE,4BAA4BC,QAAQ;QACvC,OAAO;YACLsF,IAAI;YACJC,SACE,uEACA;QACJ;IACF;IACA,MAAMnB,SAASxD,QAAOwE,kBAAAA,OAAO,CAAC,SAAS,YAAjBA,kBAAqB,IACxCnB,IAAI,GACJC,KAAK,CAAC,GAAGpD;IACZ,MAAMiE,aAAaZ,eAAeiB,OAAO,CAAC,aAAa;IACvD,MAAMH,aAAaZ,eAAee,OAAO,CAAC,aAAa;IACvD,MAAMI,WAAW/B,MAAMC,OAAO,CAACtC,IAAI,CAAC,QAAQ,IAAIA,IAAI,CAAC,QAAQ,GAAG,EAAE;IAClE,IAAIoE,SAASR,MAAM,GAAGnG,kBAAkB;QACtC,OAAO;YACLyG,IAAI;YACJC,SAAS,CAAC,8BAA8B,EAAE1G,iBAAiB,MAAM,CAAC;QACpE;IACF;IACA,MAAM4G,QAAQD,SAASjF,GAAG,CAAC+D;IAC3B,KAAK,MAAM,CAACoB,OAAO/E,KAAK,IAAI8E,MAAMf,OAAO,GAAI;QAC3C,MAAMiB,UAAUjF,yBAAyBC;QACzC,IAAIgF,SAAS,OAAO;YAAEL,IAAI;YAAOC,SAAS,CAAC,KAAK,EAAEG,QAAQ,EAAE,EAAE,EAAEC,SAAS;QAAC;IAC5E;IACA,MAAMJ,UAAUtG,mBAAmB;QACjCoG;QACAD,SAAS;YACPpF;WACIoE,SAAS;YAAEA;QAAO,IAAI,CAAC,GACvBW,WAAWC,MAAM,GAAG;YAAED;QAAW,IAAI,CAAC,GACtCE,aAAa;YAAEA;QAAW,IAAI,CAAC;QAErCQ,OAAOA;IACT;IACA,IAAIF,SAAS,OAAO;QAAED,IAAI;QAAOC;IAAQ;IACzC,MAAMK,YAAY1G,mBAChBuE,MAAMC,OAAO,CAACtC,IAAI,CAAC,YAAY,IAC3B,AAACA,IAAI,CAAC,YAAY,CAAegD,MAAM,CACrC,CAACyB,QAA2B,OAAOA,UAAU,YAE/C,EAAE;IAER,IAAI,CAACD,WAAW;QACd,OAAO;YACLN,IAAI;YACJC,SAAS;QACX;IACF;IACA,OAAO;QACLD,IAAI;QACJ9E,OAAO;YACL6E;YACAD,SAAS;gBACPpF;eACIoE,SAAS;gBAAEA;YAAO,IAAI,CAAC;gBAC3BW,YAAYA,WAAWC,MAAM,GAAGD,aAAa;gBAC7CE,YAAYF,WAAWC,MAAM,GAAIC,qBAAAA,aAAc,QAAS;;YAE1DQ,OAAOA;YACPK,SAAS1E,IAAI,CAAC,UAAU,KAAK;YAC7BwE;QACF;IACF;AACF;AAEA,sDAAsD,GACtD,OAAO,SAASG,2BACdC,UAA0D;IAE1D,MAAMC,SAASD,8BAAAA,WAAYE,aAAa;IACxC,OAAOzC,MAAMC,OAAO,CAACuC,UACjBA,OAAO7B,MAAM,CAAC,CAAC+B,KAAqB,OAAOA,OAAO,YAClD,EAAE;AACR;AAEA;;;;;;;;;CASC,GACD,OAAO,SAASC,wBACdJ,UAQa,EACbK,MAAc;IAEd,IAAI,CAACL,cAAcA,WAAWM,SAAS,EAAE;QACvC,OAAO;IACT;IACA,IAAIN,WAAWF,OAAO,KAAK,OAAO;QAChC,OAAO;IACT;IACA,IAAI,CAAC3G,cAAc6G,WAAWJ,SAAS,EAA0BS,SAAS;QACxE,OAAO;IACT;IACA,IAAIN,2BAA2BC,YAAYjC,QAAQ,CAACsC,SAAS;QAC3D,OAAO;IACT;IACA,OAAO;AACT;AAEA,uDAAuD,GACvD,OAAO,SAASE,wBACdP,UAAyD,EACzDK,MAAc;IAEd,OAAOD,wBAAwBJ,YAAYK,YAAY;AACzD;AAEA;;;;CAIC,GACD,OAAO,SAASG,mBACdN,aAAgC,EAChCN,SAA4B;IAE5B,OAAOM,cAAc9B,MAAM,CAAC,CAACiC,SAAWlH,cAAcyG,WAAWS;AACnE"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/workflows/src/lib/model/org-automations.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\nimport {\n ACTION_MAX_CONDITIONS,\n ACTION_MAX_STEPS,\n HOST_ACTION_STEP_LABELS,\n type HostActionStep,\n type HostActionStepType,\n type HostActionTriggerCondition,\n TRIGGER_COMBINATORS,\n TRIGGER_CONDITION_OPS,\n type TriggerCombinator,\n validateHostAction,\n} from '@aglyn/aglyn/app-utils/actions'\nimport {\n normalizeVisibleTo,\n type ScopeToken,\n visibleToHost,\n} from '@aglyn/aglyn/app-utils/scope-tokens'\nimport type { HostEventType } from '@aglyn/aglyn/app-utils/host-events'\n\n/**\n * ORG AUTOMATIONS (AGL-3302): one automation the organization writes once and\n * places on the sites it chooses.\n *\n * A site's own actions stay where they are, at `hosts/{hostId}/actions`. They\n * are not moved here, and the reasons are the four things a site action can\n * do that an org automation must not:\n *\n * - call the site's functions and variables through a workflow step — the\n * working set a computed variable binds to, which is one site's;\n * - run the steps only a visitor's page can run (overlays, drawers, class\n * toggles, a redirect to a screen), which the `actions` collection also\n * holds for element interactions;\n * - be fired by `/api/events/dispatch`, which is unauthenticated and ties an\n * action id to a site only by its path;\n * - wait, with the resume beat reading a missing document as the kill\n * switch, so a migration would stop every person already inside a flow.\n *\n * An org automation is therefore its own document with a narrower\n * vocabulary: triggers only the server raises, and steps that mean the same\n * thing on whichever site they run. Each run executes AS the site the event\n * happened on — that site's sender, suppressions, consent group, meters and\n * run allowance — so placing one automation on five sites is five sites each\n * running it for themselves, never one site acting for the others.\n *\n * Pure and client-safe: the console's editor, the server routes and the\n * engine read the same definitions from here.\n */\n\n/** `orgs/{orgId}/automations/{automationId}`. */\nexport const ORG_AUTOMATIONS_COLLECTION = 'automations'\n\n/**\n * The two server doors onto the collection, under the plugin's `automations`\n * API prefix: the organization's (`manage`) and a site's own pause\n * (`pause`). Named here so the console's caller and the server's registration\n * read one spelling.\n */\nexport const ORG_AUTOMATION_API_ROUTES = {\n manage: 'automations/manage',\n pause: 'automations/pause',\n} as const\n\n/**\n * How many live org automations one organization may hold.\n *\n * A flat platform cap in the shape `ACTIONS_MAX_PER_HOST` takes, and smaller,\n * because the two are different things: a site action is authored per site\n * and per element, where an org automation is a shared building block placed\n * on many sites at once. A hundred is far past what an organization writes by\n * hand and keeps the org hub's list — which reads every live one — a single\n * bounded window. Soft-deleted documents do not count.\n */\nexport const ORG_AUTOMATIONS_MAX = 100\n\n/**\n * How many org automations one event may run on one site: the same fan-out\n * bound the site's own actions take (`MAX_TRIGGERED_ACTIONS`), counted\n * separately so the two cannot crowd each other out of the query.\n */\nexport const MAX_TRIGGERED_ORG_AUTOMATIONS = 10\n\n/**\n * The run history's target type for a run of an org automation, on the\n * activity feed of the site it ran on.\n */\nexport const ORG_AUTOMATION_RUN_TARGET = 'orgAutomation'\n\n/**\n * The org activity target an org automation's own lifecycle rows are filed\n * under: the plugin's namespaced resource (`pluginId:noun`), which the org's\n * feed reads as \"Automation\" — core's target list names only core's resources.\n */\nexport const ORG_AUTOMATION_ACTIVITY_TARGET = 'workflows:automation'\n\n/**\n * The events an org automation may start on: every host event the SERVER\n * raises and a person can be the subject of.\n *\n * Listed rather than derived from `HOST_EVENT_TYPES`, deliberately. The engine\n * queries the organization's automations on every event in this list, so a\n * new host event must not become an org trigger — and a new read on every\n * occurrence of it — by being added to the platform's list. Two are left out\n * on purpose: `pageView` fires on every request to every published page, and\n * an org query on it would be a read per visitor; `memberSignOut` is raised\n * by no door today.\n */\nexport const ORG_AUTOMATION_TRIGGER_EVENTS = [\n 'formSubmission',\n 'lead',\n 'contactCreated',\n 'contactStageChanged',\n 'booking',\n 'memberSignUp',\n 'memberSignIn',\n 'dealStageChanged',\n 'dealWon',\n 'dealLost',\n 'taskCompleted',\n] as const satisfies readonly HostEventType[]\n\nexport type OrgAutomationTriggerEvent =\n (typeof ORG_AUTOMATION_TRIGGER_EVENTS)[number]\n\nconst ORG_TRIGGER_SET: ReadonlySet<string> = new Set(\n ORG_AUTOMATION_TRIGGER_EVENTS,\n)\n\n/** Whether an org automation may start on this event. */\nexport function isOrgAutomationTriggerEvent(\n event: unknown,\n): event is OrgAutomationTriggerEvent {\n return typeof event === 'string' && ORG_TRIGGER_SET.has(event)\n}\n\n/**\n * The steps an org automation may hold: the server steps that read the same\n * on every site.\n *\n * What is left out, and why:\n * - every step the visitor's page runs — overlays, drawers, menus, class\n * toggles, custom HTML and JavaScript, analytics, redirects, the site\n * alert — because an org trigger is a server event with no page to act on;\n * - `runWorkflow`, because a workflow is one site's and calls that site's\n * functions and variables;\n * - `webhookPost`, because a webhook's address and secret are one site's.\n *\n * `customEvent` stays: it fires a custom event ON the site the run is on,\n * which that site's own actions may listen for — the documented way for an\n * org automation to hand over to something site-specific.\n */\nexport const ORG_AUTOMATION_STEP_TYPES = [\n 'sendEmail',\n 'notifyAdmins',\n 'enrollList',\n 'assignCampaign',\n 'datasetAppend',\n 'updateDataset',\n 'setContactStage',\n 'addContactTag',\n 'assignContactOwner',\n 'createCrmTask',\n 'logCrmActivity',\n 'customEvent',\n 'wait',\n 'waitForEvent',\n 'exitFlow',\n] as const satisfies readonly HostActionStepType[]\n\nexport type OrgAutomationStepType = (typeof ORG_AUTOMATION_STEP_TYPES)[number]\n\nconst ORG_STEP_SET: ReadonlySet<string> = new Set(ORG_AUTOMATION_STEP_TYPES)\n\n/** Whether an org automation may run a step of this type. */\nexport function isOrgAutomationStepType(\n type: unknown,\n): type is OrgAutomationStepType {\n return typeof type === 'string' && ORG_STEP_SET.has(type)\n}\n\n/** The org editor's \"Do\" picker: each allowed step with its label, in order. */\nexport const ORG_AUTOMATION_STEP_KINDS: ReadonlyArray<{\n value: OrgAutomationStepType\n label: string\n}> = ORG_AUTOMATION_STEP_TYPES.map((value) => ({\n value,\n label: HOST_ACTION_STEP_LABELS[value],\n}))\n\n/**\n * Why an org automation cannot hold this step, or null when it can.\n *\n * Shared by the save route, which refuses it, and by the engine, which\n * refuses to run one a writer stored anyway.\n */\nexport function orgAutomationStepRefusal(step: {\n type?: unknown\n}): string | null {\n if (isOrgAutomationStepType(step?.type)) return null\n const label = HOST_ACTION_STEP_LABELS[step?.type as HostActionStepType]\n if (!label) return `“${String(step?.type ?? '')}” is not a step`\n return (\n `“${label}” belongs to one site, so an org automation cannot run it — ` +\n 'build it as an action on that site'\n )\n}\n\n/** The trigger an org automation starts on, as it is stored. */\nexport interface OrgAutomationTrigger {\n event: OrgAutomationTriggerEvent\n /** Optional expression over the payload; runs only when truthy. */\n filter?: string\n conditions: HostActionTriggerCondition[] | null\n combinator: TriggerCombinator | null\n}\n\n/**\n * The fields an org automation is written with — by the save route, and by\n * nothing else: the collection is closed to client writes.\n */\nexport interface OrgAutomationFields {\n name: string\n trigger: OrgAutomationTrigger\n steps: HostActionStep[]\n enabled: boolean\n /**\n * The sites it runs on, as scope tokens: `['org']` is every site of the\n * organization, now and later; `['host:{id}', …]` names up to thirty.\n */\n visibleTo: ScopeToken[]\n}\n\n/** `orgs/{orgId}/automations/{id}`, as it is stored. */\nexport interface OrgAutomation extends OrgAutomationFields {\n /**\n * The sites that have paused it for themselves — each a site id, never a\n * scope token. The \"host level control\": a site's admin or editor adds or\n * removes their own site here, and only their own.\n */\n pausedHostIds: string[]\n createdBy?: string\n updatedBy?: string\n /**\n * Set when it was deleted. Always written — `null` while live — so the\n * org hub's list can ask for the live ones by equality.\n */\n deletedAt: unknown\n}\n\n/** A stored org automation with its id, as the console lists it. */\nexport type OrgAutomationRow = OrgAutomation & { $id: string }\n\n/** Longest name an org automation may carry: the actions editor's cap. */\nexport const ORG_AUTOMATION_NAME_MAX = 60\n\n/** Longest trigger filter expression. */\nconst FILTER_MAX = 500\n\n/** Longest a condition's field or value may be. */\nconst CONDITION_FIELD_MAX = 64\nconst CONDITION_VALUE_MAX = 200\n\n/**\n * Every field a step of each type may carry beyond `type` and `when`, with\n * the longest a string in it may be. What the save route stores is exactly\n * this: a key a step's type does not list is dropped, so the document holds\n * the vocabulary and nothing a caller made up.\n *\n * The caps follow what the engine reads — it cuts a subject at 200 and a body\n * at 5,000 before sending — so nothing is stored that could not be run.\n */\nconst STEP_FIELDS: Record<\n OrgAutomationStepType,\n Readonly<Record<string, 'string' | 'number' | 'boolean' | number>>\n> = {\n sendEmail: { subject: 200, body: 5000, toField: 64, topicId: 128 },\n notifyAdmins: { title: 200, body: 500 },\n enrollList: { listId: 128, listName: 200 },\n assignCampaign: { campaignId: 128, campaignName: 200 },\n datasetAppend: { datasetId: 128, datasetName: 200 },\n updateDataset: { datasetId: 128, datasetName: 200 },\n setContactStage: { lifecycleStage: 64 },\n addContactTag: { tag: 200 },\n assignContactOwner: {\n ownerUid: 128,\n ownerEmail: 320,\n roundRobin: 'boolean',\n },\n createCrmTask: {\n title: 200,\n kind: 64,\n dueInDays: 'number',\n assigneeUid: 128,\n assigneeEmail: 320,\n },\n logCrmActivity: { kind: 64, body: 2000 },\n customEvent: { eventName: 64 },\n wait: { delayMinutes: 'number' },\n waitForEvent: { eventName: 64, timeoutMinutes: 'number' },\n exitFlow: {},\n}\n\nconst asObject = (value: unknown): Record<string, unknown> | null =>\n value && typeof value === 'object' && !Array.isArray(value)\n ? (value as Record<string, unknown>)\n : null\n\n/** A condition as it is stored, or null for a row that is not one. */\nfunction readCondition(raw: unknown): HostActionTriggerCondition | null {\n const row = asObject(raw)\n if (!row) return null\n const op = row['op']\n if (!(TRIGGER_CONDITION_OPS as readonly unknown[]).includes(op)) {\n // Kept as written so the validator names the missing operator rather\n // than the row silently vanishing.\n return {\n field: String(row['field'] ?? '').trim().slice(0, CONDITION_FIELD_MAX),\n op: op as never,\n }\n }\n return {\n field: String(row['field'] ?? '').trim().slice(0, CONDITION_FIELD_MAX),\n op: op as HostActionTriggerCondition['op'],\n ...(op !== 'notEmpty'\n ? { value: String(row['value'] ?? '').trim().slice(0, CONDITION_VALUE_MAX) }\n : {}),\n }\n}\n\nfunction readConditions(raw: unknown): HostActionTriggerCondition[] {\n return (Array.isArray(raw) ? raw : [])\n .slice(0, ACTION_MAX_CONDITIONS + 1)\n .map(readCondition)\n .filter((row): row is HostActionTriggerCondition => row !== null)\n}\n\nfunction readCombinator(raw: unknown): TriggerCombinator | null {\n return (TRIGGER_COMBINATORS as readonly unknown[]).includes(raw)\n ? (raw as TriggerCombinator)\n : null\n}\n\n/** One step as it would be stored: its own fields only, strings capped. */\nfunction readStep(raw: unknown): HostActionStep | { type: unknown } {\n const source = asObject(raw) ?? {}\n const type = source['type']\n if (!isOrgAutomationStepType(type)) return { type }\n const step: Record<string, unknown> = { type }\n for (const [key, kind] of Object.entries(STEP_FIELDS[type])) {\n const value = source[key]\n if (value === undefined || value === null) continue\n if (kind === 'boolean') {\n if (typeof value === 'boolean') step[key] = value\n } else if (kind === 'number') {\n if (typeof value === 'number' && Number.isFinite(value)) step[key] = value\n } else if (typeof value === 'string') {\n step[key] = value.slice(0, Number(kind))\n }\n }\n const when = asObject(source['when'])\n if (when) {\n const conditions = readConditions(when['conditions'])\n if (conditions.length) {\n step['when'] = {\n conditions,\n combinator: readCombinator(when['combinator']) ?? 'and',\n }\n }\n }\n return step as HostActionStep\n}\n\n/** What a caller sends to create or replace an org automation. */\nexport type OrgAutomationInput = Record<string, unknown>\n\n/**\n * Reads an org automation out of an untrusted request body: the fields it may\n * hold, in the shape they are stored, or the first reason it cannot be saved.\n *\n * The rules are the actions builder's own — `validateHostAction` checks the\n * name, the trigger's conditions and every step's fields — and on top of them\n * the two this vocabulary adds: the trigger must be an org trigger and every\n * step an org step. The placement must be a scope a save can store:\n * `normalizeVisibleTo` refuses an empty one and one over thirty sites rather\n * than guessing between sharing wider and taking access away.\n *\n * Whether each named site belongs to the organization is the route's to ask,\n * because it takes a read.\n */\nexport function readOrgAutomation(\n input: unknown,\n): { ok: true; value: OrgAutomationFields } | { ok: false; problem: string } {\n const body = asObject(input) ?? {}\n const name = String(body['name'] ?? '')\n .trim()\n .slice(0, ORG_AUTOMATION_NAME_MAX)\n if (!name) return { ok: false, problem: 'Name the automation' }\n const trigger = asObject(body['trigger']) ?? {}\n const event = String(trigger['event'] ?? '').trim()\n if (!isOrgAutomationTriggerEvent(event)) {\n return {\n ok: false,\n problem:\n 'Pick a trigger an org automation can start on — a form, a lead, a ' +\n 'booking, a member or a CRM event',\n }\n }\n const filter = String(trigger['filter'] ?? '')\n .trim()\n .slice(0, FILTER_MAX)\n const conditions = readConditions(trigger['conditions'])\n const combinator = readCombinator(trigger['combinator'])\n const rawSteps = Array.isArray(body['steps']) ? body['steps'] : []\n if (rawSteps.length > ACTION_MAX_STEPS) {\n return {\n ok: false,\n problem: `Org automations are capped at ${ACTION_MAX_STEPS} steps`,\n }\n }\n const steps = rawSteps.map(readStep)\n for (const [index, step] of steps.entries()) {\n const refusal = orgAutomationStepRefusal(step)\n if (refusal) return { ok: false, problem: `Step ${index + 1}: ${refusal}` }\n }\n const problem = validateHostAction({\n name,\n trigger: {\n event,\n ...(filter ? { filter } : {}),\n ...(conditions.length ? { conditions } : {}),\n ...(combinator ? { combinator } : {}),\n },\n steps: steps as HostActionStep[],\n })\n if (problem) return { ok: false, problem }\n const visibleTo = normalizeVisibleTo(\n Array.isArray(body['visibleTo'])\n ? (body['visibleTo'] as unknown[]).filter(\n (token): token is string => typeof token === 'string',\n )\n : [],\n )\n if (!visibleTo) {\n return {\n ok: false,\n problem: 'Choose every site, or up to 30 sites, for it to run on',\n }\n }\n return {\n ok: true,\n value: {\n name,\n trigger: {\n event,\n ...(filter ? { filter } : {}),\n conditions: conditions.length ? conditions : null,\n combinator: conditions.length ? (combinator ?? 'and') : null,\n },\n steps: steps as HostActionStep[],\n enabled: body['enabled'] !== false,\n visibleTo,\n },\n }\n}\n\n/** A stored document's pause list, read defensively. */\nexport function orgAutomationPausedHostIds(\n automation: { pausedHostIds?: unknown } | null | undefined,\n): string[] {\n const paused = automation?.pausedHostIds\n return Array.isArray(paused)\n ? paused.filter((id): id is string => typeof id === 'string')\n : []\n}\n\n/**\n * Why a stored org automation will not run on this site, or null when it\n * will — the one answer the engine asks when an event arrives and again when\n * a person waiting inside the automation is due.\n *\n * Deleted, switched off, taken off the site and paused there are the four\n * ways an organization or a site says \"not here\", and every one of them\n * stops the people already waiting on this site as well as the next run: the\n * kill switch a site action's resume applies, for the same reason.\n */\nexport function orgAutomationStopReason(\n automation:\n | {\n deletedAt?: unknown\n enabled?: unknown\n visibleTo?: unknown\n pausedHostIds?: unknown\n }\n | null\n | undefined,\n hostId: string,\n): string | null {\n if (!automation || automation.deletedAt) {\n return 'the org automation was deleted'\n }\n if (automation.enabled === false) {\n return 'the org automation was switched off'\n }\n if (!visibleToHost(automation.visibleTo as string[] | undefined, hostId)) {\n return 'the org automation no longer runs on this site'\n }\n if (orgAutomationPausedHostIds(automation).includes(hostId)) {\n return 'the org automation is paused on this site'\n }\n return null\n}\n\n/** Whether a stored org automation runs on this site. */\nexport function orgAutomationRunsOnHost(\n automation: Parameters<typeof orgAutomationStopReason>[0],\n hostId: string,\n): boolean {\n return orgAutomationStopReason(automation, hostId) === null\n}\n\n/**\n * A pause list after the placement changed: a site the automation no longer\n * runs on has nothing left to pause, so its entry goes with it, and a site\n * placed on it again later starts unpaused.\n */\nexport function prunePausedHostIds(\n pausedHostIds: readonly string[],\n visibleTo: readonly string[],\n): string[] {\n return pausedHostIds.filter((hostId) => visibleToHost(visibleTo, hostId))\n}\n"],"names":["ACTION_MAX_CONDITIONS","ACTION_MAX_STEPS","HOST_ACTION_STEP_LABELS","TRIGGER_COMBINATORS","TRIGGER_CONDITION_OPS","validateHostAction","normalizeVisibleTo","visibleToHost","ORG_AUTOMATIONS_COLLECTION","ORG_AUTOMATION_API_ROUTES","manage","pause","ORG_AUTOMATIONS_MAX","MAX_TRIGGERED_ORG_AUTOMATIONS","ORG_AUTOMATION_RUN_TARGET","ORG_AUTOMATION_ACTIVITY_TARGET","ORG_AUTOMATION_TRIGGER_EVENTS","ORG_TRIGGER_SET","Set","isOrgAutomationTriggerEvent","event","has","ORG_AUTOMATION_STEP_TYPES","ORG_STEP_SET","isOrgAutomationStepType","type","ORG_AUTOMATION_STEP_KINDS","map","value","label","orgAutomationStepRefusal","step","String","ORG_AUTOMATION_NAME_MAX","FILTER_MAX","CONDITION_FIELD_MAX","CONDITION_VALUE_MAX","STEP_FIELDS","sendEmail","subject","body","toField","topicId","notifyAdmins","title","enrollList","listId","listName","assignCampaign","campaignId","campaignName","datasetAppend","datasetId","datasetName","updateDataset","setContactStage","lifecycleStage","addContactTag","tag","assignContactOwner","ownerUid","ownerEmail","roundRobin","createCrmTask","kind","dueInDays","assigneeUid","assigneeEmail","logCrmActivity","customEvent","eventName","wait","delayMinutes","waitForEvent","timeoutMinutes","exitFlow","asObject","Array","isArray","readCondition","raw","row","op","includes","field","trim","slice","readConditions","filter","readCombinator","readStep","source","key","Object","entries","undefined","Number","isFinite","when","conditions","length","combinator","readOrgAutomation","input","trigger","name","ok","problem","rawSteps","steps","index","refusal","visibleTo","token","enabled","orgAutomationPausedHostIds","automation","paused","pausedHostIds","id","orgAutomationStopReason","hostId","deletedAt","orgAutomationRunsOnHost","prunePausedHostIds"],"mappings":"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"}
@@ -0,0 +1,22 @@
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
+ /**
18
+ * Called from the console registrar. The owner is named rather than read off
19
+ * the loader's marker, so the routes register the same way under a spec that
20
+ * calls the registrar directly.
21
+ */
22
+ export declare function registerWorkflowsRecordRoutes(): void;
@@ -0,0 +1,73 @@
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
+ */ import { buildRoute, Route } from "@aglyn/aglyn/app-utils/console-routes";
17
+ import { registerPluginRecordRoute } from "@aglyn/aglyn/plugin-manager/plugin-record-routes";
18
+ import { BUNDLE_ID } from "../constants/bundle-common.js";
19
+ /**
20
+ * Where a site's workflows, actions and webhooks are read, published for
21
+ * every other surface — the same three kinds this plugin indexes.
22
+ *
23
+ * The CRM installs an action recipe and names where it landed; an assistant
24
+ * job opens the actions list its drafted automation was saved to. Each asks
25
+ * the record-route registry for the kind rather than spelling this plugin's
26
+ * nav slug, and gets `null` (text instead of a link) where it is not loaded.
27
+ *
28
+ * None of the three has a page of its own: each is edited in its section of
29
+ * the Automation page, so a record's address is its section. The
30
+ * organization's hub lists every site's records under the same section ids,
31
+ * so both levels answer.
32
+ */ /** The nav slug the shell resolves this plugin's hub by, at either level. */ const AUTOMATION_SLUG = 'automation';
33
+ function section({ orgSlug, host }, id) {
34
+ const hub = host ? buildRoute(Route.HOST_PLUGIN, {
35
+ orgSlug,
36
+ host,
37
+ pluginSlug: AUTOMATION_SLUG
38
+ }) : buildRoute(Route.ORG_PLUGIN, {
39
+ orgSlug,
40
+ pluginSlug: AUTOMATION_SLUG
41
+ });
42
+ return `${hub}/${id}`;
43
+ }
44
+ /**
45
+ * Called from the console registrar. The owner is named rather than read off
46
+ * the loader's marker, so the routes register the same way under a spec that
47
+ * calls the registrar directly.
48
+ */ export function registerWorkflowsRecordRoutes() {
49
+ const owner = {
50
+ pluginId: BUNDLE_ID
51
+ };
52
+ for (const [kind, id] of [
53
+ [
54
+ 'workflow',
55
+ 'workflows'
56
+ ],
57
+ [
58
+ 'action',
59
+ 'actions'
60
+ ],
61
+ [
62
+ 'webhook',
63
+ 'webhooks'
64
+ ]
65
+ ]){
66
+ registerPluginRecordRoute(kind, {
67
+ list: (context)=>section(context, id),
68
+ record: (context)=>section(context, id)
69
+ }, owner);
70
+ }
71
+ }
72
+
73
+ //# sourceMappingURL=workflows-record-routes.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/workflows/src/lib/model/workflows-record-routes.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\nimport { buildRoute, Route } from '@aglyn/aglyn/app-utils/console-routes'\nimport {\n registerPluginRecordRoute,\n type PluginRecordRouteContext,\n} from '@aglyn/aglyn/plugin-manager/plugin-record-routes'\nimport { BUNDLE_ID } from '../constants/bundle-common'\nimport type { WorkflowsConsoleSectionId } from '../components/workflows-console-sections'\n\n/**\n * Where a site's workflows, actions and webhooks are read, published for\n * every other surface — the same three kinds this plugin indexes.\n *\n * The CRM installs an action recipe and names where it landed; an assistant\n * job opens the actions list its drafted automation was saved to. Each asks\n * the record-route registry for the kind rather than spelling this plugin's\n * nav slug, and gets `null` (text instead of a link) where it is not loaded.\n *\n * None of the three has a page of its own: each is edited in its section of\n * the Automation page, so a record's address is its section. The\n * organization's hub lists every site's records under the same section ids,\n * so both levels answer.\n */\n\n/** The nav slug the shell resolves this plugin's hub by, at either level. */\nconst AUTOMATION_SLUG = 'automation'\n\nfunction section(\n { orgSlug, host }: PluginRecordRouteContext,\n id: WorkflowsConsoleSectionId,\n): string {\n const hub = host\n ? buildRoute(Route.HOST_PLUGIN, { orgSlug, host, pluginSlug: AUTOMATION_SLUG })\n : buildRoute(Route.ORG_PLUGIN, { orgSlug, pluginSlug: AUTOMATION_SLUG })\n return `${hub}/${id}`\n}\n\n/**\n * Called from the console registrar. The owner is named rather than read off\n * the loader's marker, so the routes register the same way under a spec that\n * calls the registrar directly.\n */\nexport function registerWorkflowsRecordRoutes(): void {\n const owner = { pluginId: BUNDLE_ID }\n for (const [kind, id] of [\n ['workflow', 'workflows'],\n ['action', 'actions'],\n ['webhook', 'webhooks'],\n ] as const) {\n registerPluginRecordRoute(\n kind,\n { list: (context) => section(context, id), record: (context) => section(context, id) },\n owner,\n )\n }\n}\n"],"names":["buildRoute","Route","registerPluginRecordRoute","BUNDLE_ID","AUTOMATION_SLUG","section","orgSlug","host","id","hub","HOST_PLUGIN","pluginSlug","ORG_PLUGIN","registerWorkflowsRecordRoutes","owner","pluginId","kind","list","context","record"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED,SAASA,UAAU,EAAEC,KAAK,QAAQ,wCAAuC;AACzE,SACEC,yBAAyB,QAEpB,mDAAkD;AACzD,SAASC,SAAS,QAAQ,gCAA4B;AAGtD;;;;;;;;;;;;;CAaC,GAED,2EAA2E,GAC3E,MAAMC,kBAAkB;AAExB,SAASC,QACP,EAAEC,OAAO,EAAEC,IAAI,EAA4B,EAC3CC,EAA6B;IAE7B,MAAMC,MAAMF,OACRP,WAAWC,MAAMS,WAAW,EAAE;QAAEJ;QAASC;QAAMI,YAAYP;IAAgB,KAC3EJ,WAAWC,MAAMW,UAAU,EAAE;QAAEN;QAASK,YAAYP;IAAgB;IACxE,OAAO,GAAGK,IAAI,CAAC,EAAED,IAAI;AACvB;AAEA;;;;CAIC,GACD,OAAO,SAASK;IACd,MAAMC,QAAQ;QAAEC,UAAUZ;IAAU;IACpC,KAAK,MAAM,CAACa,MAAMR,GAAG,IAAI;QACvB;YAAC;YAAY;SAAY;QACzB;YAAC;YAAU;SAAU;QACrB;YAAC;YAAW;SAAW;KACxB,CAAW;QACVN,0BACEc,MACA;YAAEC,MAAM,CAACC,UAAYb,QAAQa,SAASV;YAAKW,QAAQ,CAACD,UAAYb,QAAQa,SAASV;QAAI,GACrFM;IAEJ;AACF"}
@@ -0,0 +1,20 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2026 Aglyn LLC
4
+ * Licensed under the Apache License, Version 2.0 (the "License");
5
+ * you may not use this file except in compliance with the License.
6
+ * You may obtain a copy of the License at
7
+ * http://www.apache.org/licenses/LICENSE-2.0
8
+ * Unless required by applicable law or agreed to in writing, software
9
+ * distributed under the License is distributed on an "AS IS" BASIS,
10
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11
+ * See the License for the specific language governing permissions and
12
+ * limitations under the License.
13
+ */
14
+ import type { ConsoleSearchSource } from '@aglyn/aglyn';
15
+ /**
16
+ * What the console's search finds in a site's automation (AGL-3080): its
17
+ * workflows, by name, opening on the site's Automation page — the address,
18
+ * never the `/workflows` one it redirects from.
19
+ */
20
+ export declare const WORKFLOWS_SEARCH_SOURCES: readonly ConsoleSearchSource[];
@@ -0,0 +1,35 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2026 Aglyn LLC
4
+ * Licensed under the Apache License, Version 2.0 (the "License");
5
+ * you may not use this file except in compliance with the License.
6
+ * You may obtain a copy of the License at
7
+ * http://www.apache.org/licenses/LICENSE-2.0
8
+ * Unless required by applicable law or agreed to in writing, software
9
+ * distributed under the License is distributed on an "AS IS" BASIS,
10
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11
+ * See the License for the specific language governing permissions and
12
+ * limitations under the License.
13
+ */ import { buildRoute, Route } from "@aglyn/aglyn/app-utils/console-routes";
14
+ /**
15
+ * What the console's search finds in a site's automation (AGL-3080): its
16
+ * workflows, by name, opening on the site's Automation page — the address,
17
+ * never the `/workflows` one it redirects from.
18
+ */ export const WORKFLOWS_SEARCH_SOURCES = [
19
+ {
20
+ id: 'workflows',
21
+ group: 'Workflows',
22
+ noun: 'workflows',
23
+ scope: 'host',
24
+ collection: 'workflows',
25
+ nameField: 'name',
26
+ entitlementKey: 'workflowsPerHost',
27
+ order: 150,
28
+ href: (_row, { orgSlug, host })=>host ? buildRoute(Route.HOST_AUTOMATION, {
29
+ orgSlug,
30
+ host
31
+ }) : null
32
+ }
33
+ ];
34
+
35
+ //# sourceMappingURL=workflows-search-sources.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/workflows/src/lib/model/workflows-search-sources.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\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 * http://www.apache.org/licenses/LICENSE-2.0\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\nimport type { ConsoleSearchSource } from '@aglyn/aglyn'\nimport { buildRoute, Route } from '@aglyn/aglyn/app-utils/console-routes'\n\n/**\n * What the console's search finds in a site's automation (AGL-3080): its\n * workflows, by name, opening on the site's Automation page — the address,\n * never the `/workflows` one it redirects from.\n */\nexport const WORKFLOWS_SEARCH_SOURCES: readonly ConsoleSearchSource[] = [\n {\n id: 'workflows',\n group: 'Workflows',\n noun: 'workflows',\n scope: 'host',\n collection: 'workflows',\n nameField: 'name',\n entitlementKey: 'workflowsPerHost',\n order: 150,\n href: (_row, { orgSlug, host }) =>\n host ? buildRoute(Route.HOST_AUTOMATION, { orgSlug, host }) : null,\n },\n]\n"],"names":["buildRoute","Route","WORKFLOWS_SEARCH_SOURCES","id","group","noun","scope","collection","nameField","entitlementKey","order","href","_row","orgSlug","host","HOST_AUTOMATION"],"mappings":"AAAA;;;;;;;;;;;;CAYC,GAGD,SAASA,UAAU,EAAEC,KAAK,QAAQ,wCAAuC;AAEzE;;;;CAIC,GACD,OAAO,MAAMC,2BAA2D;IACtE;QACEC,IAAI;QACJC,OAAO;QACPC,MAAM;QACNC,OAAO;QACPC,YAAY;QACZC,WAAW;QACXC,gBAAgB;QAChBC,OAAO;QACPC,MAAM,CAACC,MAAM,EAAEC,OAAO,EAAEC,IAAI,EAAE,GAC5BA,OAAOd,WAAWC,MAAMc,eAAe,EAAE;gBAAEF;gBAASC;YAAK,KAAK;IAClE;CACD,CAAA"}