@aglyn/plugins-workflows 1.0.0-beta.220 → 1.0.0-beta.222

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 (92) hide show
  1. package/README.md +22 -14
  2. package/package.json +12 -12
  3. package/src/lib/components/automation-step-fields.component.d.ts +1 -15
  4. package/src/lib/components/automation-step-fields.component.js +93 -10
  5. package/src/lib/components/automation-step-fields.component.js.map +1 -1
  6. package/src/lib/components/automation-trigger-conditions.component.d.ts +2 -1
  7. package/src/lib/components/automation-trigger-conditions.component.js.map +1 -1
  8. package/src/lib/components/host-actions-card.component.js +85 -77
  9. package/src/lib/components/host-actions-card.component.js.map +1 -1
  10. package/src/lib/components/host-webhooks-card.component.js +2 -1
  11. package/src/lib/components/host-webhooks-card.component.js.map +1 -1
  12. package/src/lib/components/host-workflows-card.component.js.map +1 -1
  13. package/src/lib/components/org-automation-editor.component.d.ts +2 -1
  14. package/src/lib/components/org-automation-editor.component.js.map +1 -1
  15. package/src/lib/components/org-automations-card.component.js +2 -1
  16. package/src/lib/components/org-automations-card.component.js.map +1 -1
  17. package/src/lib/components/run-quota-line.component.d.ts +17 -4
  18. package/src/lib/components/run-quota-line.component.js +16 -7
  19. package/src/lib/components/run-quota-line.component.js.map +1 -1
  20. package/src/lib/components/site-org-automations-panel.component.js +2 -1
  21. package/src/lib/components/site-org-automations-panel.component.js.map +1 -1
  22. package/src/lib/components/workflows-console-page.js +18 -7
  23. package/src/lib/components/workflows-console-page.js.map +1 -1
  24. package/src/lib/declarations.d.ts +28 -0
  25. package/src/lib/declarations.js +37 -0
  26. package/src/lib/declarations.js.map +1 -0
  27. package/src/lib/declarations.server.js +17 -0
  28. package/src/lib/declarations.server.js.map +1 -1
  29. package/src/lib/engine/run-event-actions.d.ts +5 -10
  30. package/src/lib/engine/run-event-actions.js +147 -330
  31. package/src/lib/engine/run-event-actions.js.map +1 -1
  32. package/src/lib/engine/run-event-automations.d.ts +1 -1
  33. package/src/lib/engine/run-event-automations.js.map +1 -1
  34. package/src/lib/engine/run-event-workflows.d.ts +2 -1
  35. package/src/lib/engine/run-event-workflows.js +27 -22
  36. package/src/lib/engine/run-event-workflows.js.map +1 -1
  37. package/src/lib/engine/run-meter.d.ts +98 -0
  38. package/src/lib/engine/run-meter.js +121 -0
  39. package/src/lib/engine/run-meter.js.map +1 -0
  40. package/src/lib/engine/run-org-automations.d.ts +3 -2
  41. package/src/lib/engine/run-org-automations.js +14 -11
  42. package/src/lib/engine/run-org-automations.js.map +1 -1
  43. package/src/lib/engine/workflow-steps.d.ts +1 -1
  44. package/src/lib/engine/workflow-steps.js +3 -2
  45. package/src/lib/engine/workflow-steps.js.map +1 -1
  46. package/src/lib/model/host-actions.d.ts +231 -0
  47. package/src/lib/model/host-actions.js +268 -0
  48. package/src/lib/model/host-actions.js.map +1 -0
  49. package/src/lib/model/org-automations.d.ts +2 -1
  50. package/src/lib/model/org-automations.js +6 -3
  51. package/src/lib/model/org-automations.js.map +1 -1
  52. package/src/lib/model/step-outcomes.d.ts +2 -2
  53. package/src/lib/model/step-outcomes.js +1 -1
  54. package/src/lib/model/step-outcomes.js.map +1 -1
  55. package/src/lib/plugin.js +10 -0
  56. package/src/lib/plugin.js.map +1 -1
  57. package/src/lib/server/action-test-run-route.d.ts +3 -2
  58. package/src/lib/server/action-test-run-route.js +3 -2
  59. package/src/lib/server/action-test-run-route.js.map +1 -1
  60. package/src/lib/server/org-automations-routes.d.ts +42 -1
  61. package/src/lib/server/org-automations-routes.js +120 -72
  62. package/src/lib/server/org-automations-routes.js.map +1 -1
  63. package/src/lib/server-automation-drafts.d.ts +2 -2
  64. package/src/lib/server-automation-drafts.js +9 -6
  65. package/src/lib/server-automation-drafts.js.map +1 -1
  66. package/src/lib/server.js +19 -13
  67. package/src/lib/server.js.map +1 -1
  68. package/src/lib/testing/stand-in-crm-steps.d.ts +45 -0
  69. package/src/lib/testing/stand-in-crm-steps.js +97 -0
  70. package/src/lib/testing/stand-in-crm-steps.js.map +1 -0
  71. package/src/lib/testing/stand-in-dataset-list.js +11 -3
  72. package/src/lib/testing/stand-in-dataset-list.js.map +1 -1
  73. package/src/lib/testing/stand-in-dataset-steps.d.ts +43 -0
  74. package/src/lib/testing/stand-in-dataset-steps.js +65 -0
  75. package/src/lib/testing/stand-in-dataset-steps.js.map +1 -0
  76. package/src/lib/testing/stand-in-person-records.d.ts +35 -0
  77. package/src/lib/testing/stand-in-person-records.js +54 -0
  78. package/src/lib/testing/stand-in-person-records.js.map +1 -0
  79. package/src/lib/testing/stand-in-recipes.d.ts +35 -0
  80. package/src/lib/testing/stand-in-recipes.js +201 -0
  81. package/src/lib/testing/stand-in-recipes.js.map +1 -0
  82. package/src/lib/transfer/org-automations-package.d.ts +54 -0
  83. package/src/lib/transfer/org-automations-package.js +167 -0
  84. package/src/lib/transfer/org-automations-package.js.map +1 -0
  85. package/src/lib/transfer/org-automations-package.server.d.ts +38 -0
  86. package/src/lib/transfer/org-automations-package.server.js +296 -0
  87. package/src/lib/transfer/org-automations-package.server.js.map +1 -0
  88. package/src/lib/usage-axes.js +5 -1
  89. package/src/lib/usage-axes.js.map +1 -1
  90. package/src/lib/engine/crm-action-steps.d.ts +0 -142
  91. package/src/lib/engine/crm-action-steps.js +0 -452
  92. package/src/lib/engine/crm-action-steps.js.map +0 -1
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/plugins/workflows/src/lib/engine/run-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 checkEntitlement,\n FLOW_TIMED_OUT_FIELD,\n type HostActionStep,\n} from '@aglyn/aglyn/server'\nimport { scopeTokensForHost } from '@aglyn/aglyn/app-utils/scope-tokens'\nimport {\n firebaseAdmin,\n getOrgForHost,\n resolveOrgIdForHost,\n} from '@aglyn/tenant-data-admin'\nimport type { HostEventPayload } from '@aglyn/tenant-runtime/host-event-listeners'\nimport { FieldValue } from 'firebase-admin/firestore'\nimport {\n isOrgAutomationTriggerEvent,\n MAX_TRIGGERED_ORG_AUTOMATIONS,\n ORG_AUTOMATIONS_COLLECTION,\n type OrgAutomation,\n orgAutomationRunsOnHost,\n orgAutomationStopReason,\n} from '../model/org-automations'\nimport {\n deferFlowEnrollment,\n endFlowEnrollment,\n type FlowEnrollment,\n} from './flow-enrollments'\nimport {\n type ActionRunEnv,\n executeAction,\n FLOW_CLAIM_RETRY_MS,\n makeWorkflowContextLoader,\n stopFlowEnrollment,\n} from './run-event-actions'\n\n/**\n * THE ORGANIZATION'S AUTOMATIONS, as the engine meets them (AGL-3302).\n *\n * Two doors lead here. An event arrives and the site's own actions are\n * joined by the org automations placed on the site — `runEventActions` asks\n * {@link findOrgAutomationsForEvent} and runs what it answers beside them. A\n * person waiting inside an org automation comes due, or the event they were\n * waiting for arrives, and {@link resumeOrgAutomationEnrollment} continues\n * them against the organization's document.\n *\n * Both run on the event's SITE. The run environment is the site's, so the\n * sending identity, the suppression lists, the consent group, the email meter\n * and the run allowance are all the site's — an automation placed on five\n * sites is five sites running it for themselves.\n */\n\n/** The org automations one event runs on one site, and whose they are. */\nexport interface PlacedOrgAutomations {\n orgId: string\n docs: FirebaseFirestore.QueryDocumentSnapshot[]\n}\n\n/**\n * How long a site's organization is remembered between events.\n *\n * The `hostIndex` read is the price of asking the organization at all, and it\n * repeats on every server event of a busy site and again for each event a\n * run's own steps raise. Remembered briefly rather than forever: a site that\n * changes hands is picked up within the window, and the engine refuses to run\n * a remembered organization's automations for a site whose current owner —\n * read fresh by the entitlement gate before anything runs — is another.\n */\nconst ORG_LOOKUP_TTL_MS = 60_000\n\n/** Entries kept before the memo starts over; far past one instance's working set. */\nconst ORG_LOOKUP_MAX_ENTRIES = 1_000\n\nconst orgLookups = new Map<string, { orgId: string | null; atMs: number }>()\n\n/** Test seam: forget every remembered organization. */\nexport function resetOrgAutomationLookupsForTests(): void {\n orgLookups.clear()\n}\n\nasync function orgIdForHost(hostId: string): Promise<string | null> {\n const nowMs = Date.now()\n const known = orgLookups.get(hostId)\n if (known && nowMs - known.atMs < ORG_LOOKUP_TTL_MS) return known.orgId\n const orgId = await resolveOrgIdForHost(hostId)\n if (orgLookups.size >= ORG_LOOKUP_MAX_ENTRIES) orgLookups.clear()\n orgLookups.set(hostId, { orgId, atMs: nowMs })\n return orgId\n}\n\n/**\n * The org automations this event runs on this site, or null when there are\n * none — including when the event is not one an org automation can start on,\n * which is answered before any read.\n *\n * One query: the event, switched on, and placed on this site — by the org\n * token or the site's own — capped at {@link MAX_TRIGGERED_ORG_AUTOMATIONS}.\n * `enabled` is in the query rather than filtered after it so a switched-off\n * or deleted automation (deletion switches it off too) can never take one of\n * the capped places; a PAUSE is per site and cannot be asked of an array in\n * the same query, so a paused automation is dropped here, after it.\n *\n * Served by the composite index on `automations` — `visibleTo` CONTAINS,\n * `trigger.event`, `enabled` — in `cloud/firebase-firestore.indexes.json`.\n *\n * Never throws: an organization that cannot be read runs no org automation,\n * and the site's own actions run exactly as they would have.\n */\nexport async function findOrgAutomationsForEvent(\n hostId: string,\n event: string,\n): Promise<PlacedOrgAutomations | null> {\n if (!isOrgAutomationTriggerEvent(event)) return null\n try {\n const orgId = await orgIdForHost(hostId)\n if (!orgId) return null\n const snapshot = await firebaseAdmin\n .app()\n .firestore()\n .collection('orgs')\n .doc(orgId)\n .collection(ORG_AUTOMATIONS_COLLECTION)\n .where('trigger.event', '==', event)\n .where('enabled', '==', true)\n .where('visibleTo', 'array-contains-any', scopeTokensForHost(hostId))\n .limit(MAX_TRIGGERED_ORG_AUTOMATIONS)\n .get()\n const docs = snapshot.docs.filter((doc) =>\n orgAutomationRunsOnHost(doc.data() as OrgAutomation, hostId),\n )\n return docs.length ? { orgId, docs } : null\n } catch (error) {\n console.error('[org automations] lookup failed', hostId, event, error)\n return null\n }\n}\n\n/**\n * Continues one person waiting inside an org automation, on the site they\n * were enrolled on — the org twin of the site action's resume, asking the\n * same questions of the organization's document instead of the site's.\n *\n * - The kill switch is `orgs/{orgId}/automations/{id}`: deleted, switched\n * off, taken off this site, or paused on this site, and the person stops.\n * - The site must still belong to that organization, and its plan must\n * still carry the actions builder.\n * - The snapshot runs, never the automation's current steps.\n * - Counted on this site's action-run meter, never refused by it: the person\n * is already inside, and the gate belongs at enrollment.\n */\nexport async function resumeOrgAutomationEnrollment(\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 firestore = firebaseAdmin.app().firestore()\n const hostRef = firestore.collection('hosts').doc(hostId)\n const stop = (reason: string) => stopFlowEnrollment(enrollment, ref, reason)\n\n const orgId = String(enrollment.orgId ?? '')\n const doc = orgId\n ? await firestore\n .collection('orgs')\n .doc(orgId)\n .collection(ORG_AUTOMATIONS_COLLECTION)\n .doc(enrollment.actionId)\n .get()\n .catch(() => null)\n : null\n const automation = doc?.exists ? (doc.data() as OrgAutomation) : null\n if (!automation) return await stop('the org automation was deleted')\n const refusal = orgAutomationStopReason(automation, hostId)\n if (refusal) return await stop(refusal)\n\n const owner = await getOrgForHost(hostId).catch(() => null)\n if (!owner || owner.orgId !== orgId) {\n return await stop(\n 'this site is no longer part of the organization that runs the org automation',\n )\n }\n if (!checkEntitlement(owner.org as any, 'actions')) {\n return await stop('this site’s plan no longer includes automations')\n }\n\n const env: ActionRunEnv = {\n hostId,\n hostRef,\n alerts: [],\n // Admitted by the `actions` gate above.\n actionsAllowed: true,\n webhooksAllowed: checkEntitlement(owner.org as any, 'webhooks'),\n crmAllowed: checkEntitlement(owner.org as any, 'crm'),\n depth: 0,\n org: owner.org ?? null,\n orgId: owner.orgId,\n loadWorkflowContext: makeWorkflowContextLoader(hostRef),\n }\n const payload: HostEventPayload = {\n ...((enrollment.payload ?? {}) as HostEventPayload),\n // The timeout BRANCH, as on a site action's resume.\n ...(options.timedOut ? { [FLOW_TIMED_OUT_FIELD]: true } : {}),\n }\n const ending = await executeAction(\n env,\n enrollment.actionId,\n automation,\n enrollment.event,\n payload,\n {\n startIndex: enrollment.nextStepIndex,\n steps: enrollment.steps as HostActionStep[],\n enrollmentRef: ref,\n orgAutomation: { orgId },\n },\n )\n if (ending === 'deferred') {\n await deferFlowEnrollment(ref, nowMs + FLOW_CLAIM_RETRY_MS, nowMs)\n return ending\n }\n if (ending !== 'waiting') await endFlowEnrollment(ref)\n const monthKey = new Date(nowMs).toISOString().slice(0, 7)\n await hostRef\n .collection('counters')\n .doc('actionRuns')\n .set({ [monthKey]: FieldValue.increment(1) }, { merge: true })\n .catch(() => undefined)\n return ending\n}\n"],"names":["checkEntitlement","FLOW_TIMED_OUT_FIELD","scopeTokensForHost","firebaseAdmin","getOrgForHost","resolveOrgIdForHost","FieldValue","isOrgAutomationTriggerEvent","MAX_TRIGGERED_ORG_AUTOMATIONS","ORG_AUTOMATIONS_COLLECTION","orgAutomationRunsOnHost","orgAutomationStopReason","deferFlowEnrollment","endFlowEnrollment","executeAction","FLOW_CLAIM_RETRY_MS","makeWorkflowContextLoader","stopFlowEnrollment","ORG_LOOKUP_TTL_MS","ORG_LOOKUP_MAX_ENTRIES","orgLookups","Map","resetOrgAutomationLookupsForTests","clear","orgIdForHost","hostId","nowMs","Date","now","known","get","atMs","orgId","size","set","findOrgAutomationsForEvent","event","snapshot","app","firestore","collection","doc","where","limit","docs","filter","data","length","error","console","resumeOrgAutomationEnrollment","enrollment","ref","options","owner","hostRef","stop","reason","String","actionId","catch","automation","exists","refusal","org","env","alerts","actionsAllowed","webhooksAllowed","crmAllowed","depth","loadWorkflowContext","payload","timedOut","ending","startIndex","nextStepIndex","steps","enrollmentRef","orgAutomation","monthKey","toISOString","slice","increment","merge","undefined"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/workflows/src/lib/engine/run-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 { checkEntitlement } from '@aglyn/aglyn/server'\nimport { scopeTokensForHost } from '@aglyn/aglyn/app-utils/scope-tokens'\nimport {\n firebaseAdmin,\n getOrgForHost,\n resolveOrgIdForHost,\n} from '@aglyn/tenant-data-admin'\nimport type { HostEventPayload } from '@aglyn/tenant-runtime/host-event-listeners'\nimport {\n isOrgAutomationTriggerEvent,\n MAX_TRIGGERED_ORG_AUTOMATIONS,\n ORG_AUTOMATIONS_COLLECTION,\n type OrgAutomation,\n orgAutomationRunsOnHost,\n orgAutomationStopReason,\n} from '../model/org-automations'\nimport {\n deferFlowEnrollment,\n endFlowEnrollment,\n type FlowEnrollment,\n} from './flow-enrollments'\nimport {\n type ActionRunEnv,\n executeAction,\n FLOW_CLAIM_RETRY_MS,\n makeWorkflowContextLoader,\n stopFlowEnrollment,\n} from './run-event-actions'\nimport { FLOW_TIMED_OUT_FIELD, type HostActionStep } from '../model/host-actions'\nimport { recordRuns, runMonthKey } from './run-meter'\n\n/**\n * THE ORGANIZATION'S AUTOMATIONS, as the engine meets them (AGL-3302).\n *\n * Two doors lead here. An event arrives and the site's own actions are\n * joined by the org automations placed on the site — `runEventActions` asks\n * {@link findOrgAutomationsForEvent} and runs what it answers beside them. A\n * person waiting inside an org automation comes due, or the event they were\n * waiting for arrives, and {@link resumeOrgAutomationEnrollment} continues\n * them against the organization's document.\n *\n * Both run on the event's SITE. The run environment is the site's, so the\n * sending identity, the suppression lists, the consent group, the email meter\n * and the run allowance are all the site's — an automation placed on five\n * sites is five sites running it for themselves.\n */\n\n/** The org automations one event runs on one site, and whose they are. */\nexport interface PlacedOrgAutomations {\n orgId: string\n docs: FirebaseFirestore.QueryDocumentSnapshot[]\n}\n\n/**\n * How long a site's organization is remembered between events.\n *\n * The `hostIndex` read is the price of asking the organization at all, and it\n * repeats on every server event of a busy site and again for each event a\n * run's own steps raise. Remembered briefly rather than forever: a site that\n * changes hands is picked up within the window, and the engine refuses to run\n * a remembered organization's automations for a site whose current owner —\n * read fresh by the entitlement gate before anything runs — is another.\n */\nconst ORG_LOOKUP_TTL_MS = 60_000\n\n/** Entries kept before the memo starts over; far past one instance's working set. */\nconst ORG_LOOKUP_MAX_ENTRIES = 1_000\n\nconst orgLookups = new Map<string, { orgId: string | null; atMs: number }>()\n\n/** Test seam: forget every remembered organization. */\nexport function resetOrgAutomationLookupsForTests(): void {\n orgLookups.clear()\n}\n\nasync function orgIdForHost(hostId: string): Promise<string | null> {\n const nowMs = Date.now()\n const known = orgLookups.get(hostId)\n if (known && nowMs - known.atMs < ORG_LOOKUP_TTL_MS) return known.orgId\n const orgId = await resolveOrgIdForHost(hostId)\n if (orgLookups.size >= ORG_LOOKUP_MAX_ENTRIES) orgLookups.clear()\n orgLookups.set(hostId, { orgId, atMs: nowMs })\n return orgId\n}\n\n/**\n * The org automations this event runs on this site, or null when there are\n * none — including when the event is not one an org automation can start on,\n * which is answered before any read.\n *\n * One query: the event, switched on, and placed on this site — by the org\n * token or the site's own — capped at {@link MAX_TRIGGERED_ORG_AUTOMATIONS}.\n * `enabled` is in the query rather than filtered after it so a switched-off\n * or deleted automation (deletion switches it off too) can never take one of\n * the capped places; a PAUSE is per site and cannot be asked of an array in\n * the same query, so a paused automation is dropped here, after it.\n *\n * Served by the composite index on `automations` — `visibleTo` CONTAINS,\n * `trigger.event`, `enabled` — in `cloud/firebase-firestore.indexes.json`.\n *\n * Never throws: an organization that cannot be read runs no org automation,\n * and the site's own actions run exactly as they would have.\n */\nexport async function findOrgAutomationsForEvent(\n hostId: string,\n event: string,\n): Promise<PlacedOrgAutomations | null> {\n if (!isOrgAutomationTriggerEvent(event)) return null\n try {\n const orgId = await orgIdForHost(hostId)\n if (!orgId) return null\n const snapshot = await firebaseAdmin\n .app()\n .firestore()\n .collection('orgs')\n .doc(orgId)\n .collection(ORG_AUTOMATIONS_COLLECTION)\n .where('trigger.event', '==', event)\n .where('enabled', '==', true)\n .where('visibleTo', 'array-contains-any', scopeTokensForHost(hostId))\n .limit(MAX_TRIGGERED_ORG_AUTOMATIONS)\n .get()\n const docs = snapshot.docs.filter((doc) =>\n orgAutomationRunsOnHost(doc.data() as OrgAutomation, hostId),\n )\n return docs.length ? { orgId, docs } : null\n } catch (error) {\n console.error('[org automations] lookup failed', hostId, event, error)\n return null\n }\n}\n\n/**\n * Continues one person waiting inside an org automation, on the site they\n * were enrolled on — the org twin of the site action's resume, asking the\n * same questions of the organization's document instead of the site's.\n *\n * - The kill switch is `orgs/{orgId}/automations/{id}`: deleted, switched\n * off, taken off this site, or paused on this site, and the person stops.\n * - The site must still belong to that organization, and its plan must\n * still carry the actions builder.\n * - The snapshot runs, never the automation's current steps.\n * - Counted on this site's action-run meter and the workspace's, never\n * refused by them: the person is already inside, and the gate belongs at\n * enrollment.\n */\nexport async function resumeOrgAutomationEnrollment(\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 firestore = firebaseAdmin.app().firestore()\n const hostRef = firestore.collection('hosts').doc(hostId)\n const stop = (reason: string) => stopFlowEnrollment(enrollment, ref, reason)\n\n const orgId = String(enrollment.orgId ?? '')\n const doc = orgId\n ? await firestore\n .collection('orgs')\n .doc(orgId)\n .collection(ORG_AUTOMATIONS_COLLECTION)\n .doc(enrollment.actionId)\n .get()\n .catch(() => null)\n : null\n const automation = doc?.exists ? (doc.data() as OrgAutomation) : null\n if (!automation) return await stop('the org automation was deleted')\n const refusal = orgAutomationStopReason(automation, hostId)\n if (refusal) return await stop(refusal)\n\n const owner = await getOrgForHost(hostId).catch(() => null)\n if (!owner || owner.orgId !== orgId) {\n return await stop(\n 'this site is no longer part of the organization that runs the org automation',\n )\n }\n if (!checkEntitlement(owner.org as any, 'actions')) {\n return await stop('this site’s plan no longer includes automations')\n }\n\n const env: ActionRunEnv = {\n hostId,\n hostRef,\n alerts: [],\n // Admitted by the `actions` gate above.\n actionsAllowed: true,\n webhooksAllowed: checkEntitlement(owner.org as any, 'webhooks'),\n depth: 0,\n org: owner.org ?? null,\n orgId: owner.orgId,\n loadWorkflowContext: makeWorkflowContextLoader(hostRef),\n }\n const payload: HostEventPayload = {\n ...((enrollment.payload ?? {}) as HostEventPayload),\n // The timeout BRANCH, as on a site action's resume.\n ...(options.timedOut ? { [FLOW_TIMED_OUT_FIELD]: true } : {}),\n }\n const ending = await executeAction(\n env,\n enrollment.actionId,\n automation,\n enrollment.event,\n payload,\n {\n startIndex: enrollment.nextStepIndex,\n steps: enrollment.steps as HostActionStep[],\n enrollmentRef: ref,\n orgAutomation: { orgId },\n },\n )\n if (ending === 'deferred') {\n await deferFlowEnrollment(ref, nowMs + FLOW_CLAIM_RETRY_MS, nowMs)\n return ending\n }\n if (ending !== 'waiting') await endFlowEnrollment(ref)\n await recordRuns({\n firestore,\n hostRef,\n orgId: owner.orgId,\n counter: 'actionRuns',\n month: runMonthKey(nowMs),\n count: 1,\n })\n return ending\n}\n"],"names":["checkEntitlement","scopeTokensForHost","firebaseAdmin","getOrgForHost","resolveOrgIdForHost","isOrgAutomationTriggerEvent","MAX_TRIGGERED_ORG_AUTOMATIONS","ORG_AUTOMATIONS_COLLECTION","orgAutomationRunsOnHost","orgAutomationStopReason","deferFlowEnrollment","endFlowEnrollment","executeAction","FLOW_CLAIM_RETRY_MS","makeWorkflowContextLoader","stopFlowEnrollment","FLOW_TIMED_OUT_FIELD","recordRuns","runMonthKey","ORG_LOOKUP_TTL_MS","ORG_LOOKUP_MAX_ENTRIES","orgLookups","Map","resetOrgAutomationLookupsForTests","clear","orgIdForHost","hostId","nowMs","Date","now","known","get","atMs","orgId","size","set","findOrgAutomationsForEvent","event","snapshot","app","firestore","collection","doc","where","limit","docs","filter","data","length","error","console","resumeOrgAutomationEnrollment","enrollment","ref","options","owner","hostRef","stop","reason","String","actionId","catch","automation","exists","refusal","org","env","alerts","actionsAllowed","webhooksAllowed","depth","loadWorkflowContext","payload","timedOut","ending","startIndex","nextStepIndex","steps","enrollmentRef","orgAutomation","counter","month","count"],"mappings":"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"}
@@ -14,8 +14,8 @@
14
14
  * See the License for the specific language governing permissions and
15
15
  * limitations under the License.
16
16
  */
17
- import { type HostActionStep, type HostActionStepType } from '@aglyn/aglyn/app-utils/actions';
18
17
  import { type HostWorkflow, type HostWorkflowStep } from '../model/workflows';
18
+ import { type HostActionStep, type HostActionStepType } from '../model/host-actions';
19
19
  /**
20
20
  * ONE STEP MODEL FOR BOTH ENGINES.
21
21
  *
@@ -14,8 +14,9 @@ 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 { HOST_ACTION_STEP_LABELS, isClientActionStep, validateHostAction } from "@aglyn/aglyn/app-utils/actions";
18
- import { WORKFLOW_MAX_STEPS } from "../model/workflows.js";
17
+ */ import { WORKFLOW_MAX_STEPS } from "../model/workflows.js";
18
+ import { isClientActionStep } from "@aglyn/aglyn/app-utils/site-interactions";
19
+ import { HOST_ACTION_STEP_LABELS, validateHostAction } from "../model/host-actions.js";
19
20
  /** Whether a stored step is an Actions step rather than a function call. */ export function isWorkflowActionStep(step) {
20
21
  const type = step == null ? void 0 : step.type;
21
22
  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 '../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
+ {"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 type HostWorkflow,\n type HostWorkflowStep,\n WORKFLOW_MAX_STEPS,\n} from '../model/workflows'\nimport { isClientActionStep } from '@aglyn/aglyn/app-utils/site-interactions'\nimport {\n HOST_ACTION_STEP_LABELS,\n type HostActionStep,\n type HostActionStepType,\n validateHostAction,\n} from '../model/host-actions'\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":["WORKFLOW_MAX_STEPS","isClientActionStep","HOST_ACTION_STEP_LABELS","validateHostAction","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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"}
@@ -0,0 +1,231 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2026 Aglyn LLC
4
+ *
5
+ * Licensed under the Apache License, Version 2.0 (the "License");
6
+ * you may not use this file except in compliance with the License.
7
+ * You may obtain a copy of the License at
8
+ *
9
+ * http://www.apache.org/licenses/LICENSE-2.0
10
+ *
11
+ * Unless required by applicable law or agreed to in writing, software
12
+ * distributed under the License is distributed on an "AS IS" BASIS,
13
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ * See the License for the specific language governing permissions and
15
+ * limitations under the License.
16
+ */
17
+ import { type ContactLifecycleStage, type CrmActivityKind, type CrmTaskKind } from '@aglyn/aglyn/app-utils/crm-kinds';
18
+ import type { CrmActivityDirection, CrmTaskPriority } from '@aglyn/aglyn/app-utils/crm';
19
+ import { type ClientInteractionStep, type InteractionCondition, type InteractionStepBase, type InteractionStepGuard, type InteractionTrigger, type SiteAlert, type SiteInteraction } from '@aglyn/aglyn/app-utils/site-interactions';
20
+ /**
21
+ * THE AUTOMATION VOCABULARY (AGL-148): HubSpot-style event → action
22
+ * automation on top of the AGL-128 event triggers. An action listens for a
23
+ * host event (built-in or a custom name fired by another action), optionally
24
+ * filters on the payload, and runs an ordered step list. Pure types and
25
+ * validation; the engine that runs them is this plugin's (`engine/`).
26
+ *
27
+ * An action is a SITE INTERACTION (core `site-interactions.ts`) with server
28
+ * steps added: its trigger, its conditions, its client steps, its stored shape
29
+ * (`siteInteractionDocument`), the slice a visitor's page receives
30
+ * (`interactionStepsForClient`) and its recipe stamp are the platform's. What
31
+ * is declared below is the automation's own — the server steps' shapes, the
32
+ * flow's bounds and each server step's checks. How each step is named, which
33
+ * steps hold a run and which fields a person types words into are declared in
34
+ * `plugins.config.json` (`interactionSteps`), so a page, a drafter and a
35
+ * validator read them without loading this plugin; the checks are registered
36
+ * from this plugin's declarations (`interaction-step-checks`).
37
+ */
38
+ /** The automation's names for the platform's interaction shapes. */
39
+ export type HostActionTriggerCondition = InteractionCondition;
40
+ export type HostActionTrigger = InteractionTrigger;
41
+ export type HostActionStepGuard = InteractionStepGuard;
42
+ /** Alert produced by a `siteAlert` step, surfaced to the emitting client. */
43
+ export type HostActionAlert = SiteAlert;
44
+ /**
45
+ * The scope key a resumed flow carries to say the wait ended on the CLOCK
46
+ * rather than on the event it was watching for.
47
+ *
48
+ * This is how a `waitForEvent` gets its timeout branch without a nested step
49
+ * list: the flow resumes either way, and the step after it carries a `when`
50
+ * naming this field. Underscored because it shares a namespace with the
51
+ * event payload's own fields, which are merchant-authored form field names.
52
+ * Declared as the step's `holds.timeoutField` for readers that do not load
53
+ * this plugin; this plugin's spec holds the two equal.
54
+ */
55
+ export declare const FLOW_TIMED_OUT_FIELD = "_waitTimedOut";
56
+ /** The steps an automation adds to the platform's client steps: the server's, run by the workflows plugin. */
57
+ type ServerActionStep = ({
58
+ type: 'runWorkflow';
59
+ workflowId?: string;
60
+ workflowName?: string;
61
+ } | {
62
+ type: 'customEvent';
63
+ eventName: string;
64
+ } | {
65
+ type: 'datasetAppend';
66
+ datasetId?: string;
67
+ datasetName?: string;
68
+ } | {
69
+ type: 'webhookPost';
70
+ webhookId?: string;
71
+ webhookName?: string;
72
+ } | {
73
+ type: 'sendEmail';
74
+ subject: string;
75
+ body: string;
76
+ toField?: string;
77
+ /**
78
+ * The stream this message belongs to, so a recipient who left it is not
79
+ * mailed. Absent on every step authored before topics reached the
80
+ * actions editor, which the executor resolves to the default topic.
81
+ */
82
+ topicId?: string;
83
+ } | {
84
+ type: 'notifyAdmins';
85
+ title: string;
86
+ body?: string;
87
+ } | {
88
+ type: 'enrollList';
89
+ listId?: string;
90
+ listName?: string;
91
+ } | {
92
+ type: 'updateDataset';
93
+ datasetId?: string;
94
+ datasetName?: string;
95
+ } | {
96
+ type: 'assignCampaign';
97
+ campaignId?: string;
98
+ campaignName?: string;
99
+ } | {
100
+ type: 'wait';
101
+ /** Whole minutes to hold before the next step. */
102
+ delayMinutes: number;
103
+ } | {
104
+ type: 'waitForEvent';
105
+ /** The host or custom event that resumes this person's flow. */
106
+ eventName: string;
107
+ /**
108
+ * Whole minutes after which the flow continues anyway, with
109
+ * {@link FLOW_TIMED_OUT_FIELD} true in scope. There is always a
110
+ * deadline: a wait with no timeout is an enrollment that lives forever.
111
+ */
112
+ timeoutMinutes: number;
113
+ }
114
+ /** Ends the enrollment here. Paired with a `when`, this is the exit branch. */
115
+ | {
116
+ type: 'exitFlow';
117
+ } | {
118
+ type: 'setContactStage';
119
+ lifecycleStage: ContactLifecycleStage;
120
+ } | {
121
+ type: 'addContactTag';
122
+ tag: string;
123
+ }
124
+ /**
125
+ * The owner by uid when a member id was typed or a picker wrote the step,
126
+ * by email when an address was typed; the executor resolves either against
127
+ * the org's roster, and only the roster (AGL-2614).
128
+ * Or `roundRobin`, and the owner is the next member of the pool the CRM's
129
+ * settings keep (AGL-2618) — a step that names nobody and hands the
130
+ * choice to the rotation, so a stage change can spread its follow-ups
131
+ * across a team rather than pile them on one rep. The two are exclusive:
132
+ * a rotation step carries no member, and the validator refuses one that
133
+ * names both.
134
+ */
135
+ | {
136
+ type: 'assignContactOwner';
137
+ ownerUid?: string;
138
+ ownerEmail?: string;
139
+ roundRobin?: boolean;
140
+ } | {
141
+ type: 'createCrmTask';
142
+ title: string;
143
+ /** The task's Type, by meaning; the run stores the org's label for it (AGL-3517). */
144
+ kind: CrmTaskKind;
145
+ /** Its Priority, by meaning — `normal` when absent. */
146
+ priority?: CrmTaskPriority;
147
+ /** Days from the run to the due date; `0` is due today. */
148
+ dueInDays: number;
149
+ /**
150
+ * Who gets it — by uid or by an address, both resolved against the
151
+ * roster the way the owner step's are. Neither named, the task goes
152
+ * to the contact's owner, then to nobody.
153
+ */
154
+ assigneeUid?: string;
155
+ assigneeEmail?: string;
156
+ } | {
157
+ type: 'logCrmActivity';
158
+ kind: CrmActivityKind;
159
+ body: string;
160
+ /** Which way a call or an email went (AGL-3517); no other kind takes one. */
161
+ direction?: CrmActivityDirection;
162
+ }) & {
163
+ /** See {@link InteractionStepGuard}. Absent means the step always runs. */
164
+ when?: InteractionStepGuard | null;
165
+ };
166
+ /** A step of an automation: one of the platform's client steps, or one of its server steps. */
167
+ export type HostActionStep = ClientInteractionStep | ServerActionStep;
168
+ /**
169
+ * The steps that suspend a run and continue it later, from a job beat.
170
+ *
171
+ * Named as a set rather than checked inline because three surfaces have to
172
+ * agree on it: the executor stops here and writes an enrollment, the client
173
+ * payload is truncated here, and the validator refuses a flow that waits
174
+ * without a person to wait for. A visitor's page and a drafter read the same
175
+ * fact from each step's declaration (`holds`, `interactionSteps` in
176
+ * `plugins.config.json`), which this plugin's spec holds to this set.
177
+ */
178
+ export declare const FLOW_SUSPENDING_STEP_TYPES: ReadonlySet<HostActionStepType>;
179
+ export declare function isFlowSuspendingStep(step: HostActionStep): boolean;
180
+ export type HostActionStepType = HostActionStep['type'];
181
+ /**
182
+ * `hosts/{hostId}/actions/{id}` doc: a site interaction whose steps may be
183
+ * the automation's server steps as well as the platform's client steps.
184
+ */
185
+ export type HostAction = SiteInteraction<HostActionStep>;
186
+ /**
187
+ * The shortest and longest a flow may wait.
188
+ *
189
+ * The floor is a minute because the resume beat runs on a minute, so anything
190
+ * under one is a delay the scheduler cannot honor and would only read as
191
+ * imprecision. The ceiling is ninety days: long enough for the win-back that
192
+ * is the longest sequence anybody writes, and short enough that an enrollment
193
+ * is not an unbounded lease on a document. A person waiting inside a flow is
194
+ * storage the merchant is not looking at, and a wait measured in years is
195
+ * indistinguishable from one nobody will ever collect. Declared as the band
196
+ * each waiting step `holds`; this plugin's spec holds the two equal.
197
+ */
198
+ export declare const FLOW_WAIT_MIN_MINUTES = 1;
199
+ export declare const FLOW_WAIT_MAX_MINUTES: number;
200
+ /** Every step type an action may hold, in the picker's order. */
201
+ export declare const HOST_ACTION_STEP_TYPES: readonly HostActionStepType[];
202
+ /** The server steps among them: every type that is not one of the platform's client steps. */
203
+ export declare const SERVER_ACTION_STEP_TYPES: readonly HostActionStepType[];
204
+ /**
205
+ * How each step reads in the editor's picker, a run history and a sentence:
206
+ * the platform's label for a client step, and the label its declaration
207
+ * gives every other (`interactionSteps` in `plugins.config.json`), so a page,
208
+ * a drafter and this editor name a step alike.
209
+ */
210
+ export declare const HOST_ACTION_STEP_LABELS: Readonly<Record<HostActionStepType, string>>;
211
+ /** A whole number of minutes inside the wait band. */
212
+ export declare function isFlowWaitMinutes(value: unknown): boolean;
213
+ /** True for a custom (non-built-in) event name an action may fire. */
214
+ export declare function isCustomEventName(event: string): boolean;
215
+ /**
216
+ * Validates an action doc shape; returns a human-readable error or null.
217
+ * Server and console share this so bad steps never persist or run.
218
+ */
219
+ export declare function validateHostAction(action: HostAction): string | null;
220
+ /**
221
+ * A server step's own complaint, or null. A `runWorkflow` step's pick is
222
+ * checked with every other declared step's (`interactionSteps` in
223
+ * `plugins.config.json`), by `validateInteraction`.
224
+ *
225
+ * Registered from this plugin's declarations as the check of every server
226
+ * step (`interaction-step-checks`), so a plugin that writes an automation it
227
+ * does not edit — a recipe installed into a site — refuses what this editor
228
+ * would.
229
+ */
230
+ export declare function hostActionStepProblem(candidate: InteractionStepBase, label: string): string | null;
231
+ export {};
@@ -0,0 +1,268 @@
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 { CONTACT_TAG_MAX_LENGTH, CRM_TASK_MAX_DUE_DAYS, isContactLifecycleStage, isCrmActivityKind, isCrmTaskKind } from "@aglyn/aglyn/app-utils/crm-kinds";
17
+ import { HOST_EVENT_TYPES } from "@aglyn/aglyn/app-utils/host-events";
18
+ import { CLIENT_ACTION_STEP_TYPES, CUSTOM_EVENT_PATTERN, interactionStepLabel, validateInteraction } from "@aglyn/aglyn/app-utils/site-interactions";
19
+ /** A task's priorities, by meaning (AGL-3517) — the `taskPriority` picklist's meanings. */ const TASK_PRIORITIES = [
20
+ 'low',
21
+ 'normal',
22
+ 'high'
23
+ ];
24
+ /** The directions a logged call or email takes (AGL-3517) — `CRM_ACTIVITY_DIRECTIONS`. */ const ACTIVITY_DIRECTIONS = {
25
+ call: [
26
+ 'outbound',
27
+ 'inbound',
28
+ 'internal'
29
+ ],
30
+ email: [
31
+ 'outbound',
32
+ 'inbound'
33
+ ]
34
+ };
35
+ /**
36
+ * The scope key a resumed flow carries to say the wait ended on the CLOCK
37
+ * rather than on the event it was watching for.
38
+ *
39
+ * This is how a `waitForEvent` gets its timeout branch without a nested step
40
+ * list: the flow resumes either way, and the step after it carries a `when`
41
+ * naming this field. Underscored because it shares a namespace with the
42
+ * event payload's own fields, which are merchant-authored form field names.
43
+ * Declared as the step's `holds.timeoutField` for readers that do not load
44
+ * this plugin; this plugin's spec holds the two equal.
45
+ */ export const FLOW_TIMED_OUT_FIELD = '_waitTimedOut';
46
+ /**
47
+ * The steps that suspend a run and continue it later, from a job beat.
48
+ *
49
+ * Named as a set rather than checked inline because three surfaces have to
50
+ * agree on it: the executor stops here and writes an enrollment, the client
51
+ * payload is truncated here, and the validator refuses a flow that waits
52
+ * without a person to wait for. A visitor's page and a drafter read the same
53
+ * fact from each step's declaration (`holds`, `interactionSteps` in
54
+ * `plugins.config.json`), which this plugin's spec holds to this set.
55
+ */ export const FLOW_SUSPENDING_STEP_TYPES = new Set([
56
+ 'wait',
57
+ 'waitForEvent'
58
+ ]);
59
+ export function isFlowSuspendingStep(step) {
60
+ return FLOW_SUSPENDING_STEP_TYPES.has(step.type);
61
+ }
62
+ /**
63
+ * The shortest and longest a flow may wait.
64
+ *
65
+ * The floor is a minute because the resume beat runs on a minute, so anything
66
+ * under one is a delay the scheduler cannot honor and would only read as
67
+ * imprecision. The ceiling is ninety days: long enough for the win-back that
68
+ * is the longest sequence anybody writes, and short enough that an enrollment
69
+ * is not an unbounded lease on a document. A person waiting inside a flow is
70
+ * storage the merchant is not looking at, and a wait measured in years is
71
+ * indistinguishable from one nobody will ever collect. Declared as the band
72
+ * each waiting step `holds`; this plugin's spec holds the two equal.
73
+ */ export const FLOW_WAIT_MIN_MINUTES = 1;
74
+ export const FLOW_WAIT_MAX_MINUTES = 90 * 24 * 60;
75
+ /**
76
+ * Every step an action may hold, in the order the editor's "Do" picker offers
77
+ * them. Total over the step types: a type the vocabulary gains is a compile
78
+ * error here until it is placed.
79
+ */ const STEP_ORDER = {
80
+ runWorkflow: 0,
81
+ siteAlert: 1,
82
+ customEvent: 2,
83
+ datasetAppend: 3,
84
+ webhookPost: 4,
85
+ showOverlay: 5,
86
+ stickyNav: 6,
87
+ addClass: 7,
88
+ toggleClass: 8,
89
+ removeClass: 9,
90
+ showElement: 10,
91
+ hideElement: 11,
92
+ toggleElement: 12,
93
+ openDrawer: 13,
94
+ closeDrawer: 14,
95
+ toggleDrawer: 15,
96
+ openMenu: 16,
97
+ closeMenu: 17,
98
+ toggleMenu: 18,
99
+ setAttribute: 19,
100
+ removeAttribute: 20,
101
+ scrollTo: 21,
102
+ playVideo: 22,
103
+ showHtml: 23,
104
+ runJs: 24,
105
+ redirect: 25,
106
+ trackGaEvent: 26,
107
+ sendEmail: 27,
108
+ notifyAdmins: 28,
109
+ enrollList: 29,
110
+ updateDataset: 30,
111
+ assignCampaign: 31,
112
+ wait: 32,
113
+ waitForEvent: 33,
114
+ exitFlow: 34,
115
+ setContactStage: 35,
116
+ addContactTag: 36,
117
+ assignContactOwner: 37,
118
+ createCrmTask: 38,
119
+ logCrmActivity: 39
120
+ };
121
+ /** Every step type an action may hold, in the picker's order. */ export const HOST_ACTION_STEP_TYPES = Object.keys(STEP_ORDER).sort((a, b)=>STEP_ORDER[a] - STEP_ORDER[b]);
122
+ /** The server steps among them: every type that is not one of the platform's client steps. */ export const SERVER_ACTION_STEP_TYPES = HOST_ACTION_STEP_TYPES.filter((type)=>!CLIENT_ACTION_STEP_TYPES.has(type));
123
+ /**
124
+ * How each step reads in the editor's picker, a run history and a sentence:
125
+ * the platform's label for a client step, and the label its declaration
126
+ * gives every other (`interactionSteps` in `plugins.config.json`), so a page,
127
+ * a drafter and this editor name a step alike.
128
+ */ export const HOST_ACTION_STEP_LABELS = Object.fromEntries(HOST_ACTION_STEP_TYPES.map((type)=>{
129
+ var _interactionStepLabel;
130
+ return [
131
+ type,
132
+ (_interactionStepLabel = interactionStepLabel(type)) != null ? _interactionStepLabel : type
133
+ ];
134
+ }));
135
+ /** A whole number of minutes inside the wait band. */ export function isFlowWaitMinutes(value) {
136
+ return Number.isInteger(value) && value >= FLOW_WAIT_MIN_MINUTES && value <= FLOW_WAIT_MAX_MINUTES;
137
+ }
138
+ /** True for a custom (non-built-in) event name an action may fire. */ export function isCustomEventName(event) {
139
+ return !HOST_EVENT_TYPES.includes(event) && CUSTOM_EVENT_PATTERN.test(event);
140
+ }
141
+ /**
142
+ * Validates an action doc shape; returns a human-readable error or null.
143
+ * Server and console share this so bad steps never persist or run.
144
+ */ export function validateHostAction(action) {
145
+ // The name, the recipe stamp, the trigger, the step guards, the client
146
+ // steps and every declared step's pick are the platform's to check; the
147
+ // server steps are this module's.
148
+ return validateInteraction(action, {
149
+ validateStep: (step, label)=>hostActionStepProblem(step, label)
150
+ });
151
+ }
152
+ /**
153
+ * A server step's own complaint, or null. A `runWorkflow` step's pick is
154
+ * checked with every other declared step's (`interactionSteps` in
155
+ * `plugins.config.json`), by `validateInteraction`.
156
+ *
157
+ * Registered from this plugin's declarations as the check of every server
158
+ * step (`interaction-step-checks`), so a plugin that writes an automation it
159
+ * does not edit — a recipe installed into a site — refuses what this editor
160
+ * would.
161
+ */ export function hostActionStepProblem(candidate, label) {
162
+ var _step_datasetId, _step_datasetName, _step_webhookId, _step_webhookName, _step_title, _step_listId, _step_listName, _step_datasetId1, _step_datasetName1, _step_campaignId, _step_campaignName;
163
+ const step = candidate;
164
+ if (step.type === 'wait' && !isFlowWaitMinutes(step.delayMinutes)) {
165
+ return `${label}: wait between ${FLOW_WAIT_MIN_MINUTES} minute and ${FLOW_WAIT_MAX_MINUTES} minutes`;
166
+ }
167
+ if (step.type === 'waitForEvent') {
168
+ var _ref;
169
+ var _step_eventName;
170
+ const waited = (_ref = (_step_eventName = step.eventName) == null ? void 0 : _step_eventName.trim()) != null ? _ref : '';
171
+ if (!waited || !HOST_EVENT_TYPES.includes(waited) && !isCustomEventName(waited)) {
172
+ return `${label}: pick the event to wait for`;
173
+ }
174
+ if (!isFlowWaitMinutes(step.timeoutMinutes)) {
175
+ return `${label}: give up after ${FLOW_WAIT_MIN_MINUTES}–${FLOW_WAIT_MAX_MINUTES} minutes`;
176
+ }
177
+ }
178
+ if (step.type === 'customEvent') {
179
+ var _ref1;
180
+ var _step_eventName1;
181
+ if (!isCustomEventName((_ref1 = (_step_eventName1 = step.eventName) == null ? void 0 : _step_eventName1.trim()) != null ? _ref1 : '')) {
182
+ return `${label}: custom event names are 2–40 letters, digits, dashes`;
183
+ }
184
+ }
185
+ if (step.type === 'datasetAppend' && !((_step_datasetId = step.datasetId) == null ? void 0 : _step_datasetId.trim()) && !((_step_datasetName = step.datasetName) == null ? void 0 : _step_datasetName.trim())) {
186
+ return `${label}: pick a dataset`;
187
+ }
188
+ if (step.type === 'webhookPost' && !((_step_webhookId = step.webhookId) == null ? void 0 : _step_webhookId.trim()) && !((_step_webhookName = step.webhookName) == null ? void 0 : _step_webhookName.trim())) {
189
+ return `${label}: pick a webhook`;
190
+ }
191
+ if (step.type === 'sendEmail') {
192
+ var _step_subject, _step_body;
193
+ if (!((_step_subject = step.subject) == null ? void 0 : _step_subject.trim())) return `${label}: enter the subject`;
194
+ if (!((_step_body = step.body) == null ? void 0 : _step_body.trim())) return `${label}: enter the email body`;
195
+ }
196
+ if (step.type === 'notifyAdmins' && !((_step_title = step.title) == null ? void 0 : _step_title.trim())) {
197
+ return `${label}: enter the notification title`;
198
+ }
199
+ if (step.type === 'enrollList' && !((_step_listId = step.listId) == null ? void 0 : _step_listId.trim()) && !((_step_listName = step.listName) == null ? void 0 : _step_listName.trim())) {
200
+ return `${label}: pick a list`;
201
+ }
202
+ if (step.type === 'updateDataset' && !((_step_datasetId1 = step.datasetId) == null ? void 0 : _step_datasetId1.trim()) && !((_step_datasetName1 = step.datasetName) == null ? void 0 : _step_datasetName1.trim())) {
203
+ return `${label}: pick a dataset`;
204
+ }
205
+ if (step.type === 'assignCampaign' && !((_step_campaignId = step.campaignId) == null ? void 0 : _step_campaignId.trim()) && !((_step_campaignName = step.campaignName) == null ? void 0 : _step_campaignName.trim())) {
206
+ return `${label}: pick a campaign`;
207
+ }
208
+ // CRM steps (AGL-2605). A stage or a kind outside its vocabulary is
209
+ // refused here rather than stored: the executor treats the value as
210
+ // trusted and would write it into a facet every stage report counts.
211
+ if (step.type === 'setContactStage' && !isContactLifecycleStage(step.lifecycleStage)) {
212
+ return `${label}: pick a lifecycle stage`;
213
+ }
214
+ if (step.type === 'addContactTag') {
215
+ var _ref2;
216
+ var _step_tag;
217
+ const tag = (_ref2 = (_step_tag = step.tag) == null ? void 0 : _step_tag.trim()) != null ? _ref2 : '';
218
+ if (!tag) return `${label}: enter the tag`;
219
+ if (tag.length > CONTACT_TAG_MAX_LENGTH) {
220
+ return `${label}: tags are at most ${CONTACT_TAG_MAX_LENGTH} characters`;
221
+ }
222
+ }
223
+ if (step.type === 'assignContactOwner') {
224
+ var _step_ownerUid, _step_ownerEmail, _step_ownerUid1, _step_ownerEmail1;
225
+ const named = Boolean(((_step_ownerUid = step.ownerUid) == null ? void 0 : _step_ownerUid.trim()) || ((_step_ownerEmail = step.ownerEmail) == null ? void 0 : _step_ownerEmail.trim()));
226
+ if (step.roundRobin === true && named) {
227
+ return `${label}: pick round robin or a member, not both`;
228
+ }
229
+ if (step.roundRobin !== true && !((_step_ownerUid1 = step.ownerUid) == null ? void 0 : _step_ownerUid1.trim()) && !((_step_ownerEmail1 = step.ownerEmail) == null ? void 0 : _step_ownerEmail1.trim().includes('@'))) {
230
+ return `${label}: enter the owner’s email address`;
231
+ }
232
+ }
233
+ if (step.type === 'createCrmTask') {
234
+ var _ref3;
235
+ var _step_title1, _step_assigneeEmail;
236
+ if (!((_step_title1 = step.title) == null ? void 0 : _step_title1.trim())) return `${label}: give the task a title`;
237
+ if (!isCrmTaskKind(step.kind)) return `${label}: pick the type of task`;
238
+ if (step.priority !== undefined && !TASK_PRIORITIES.includes(step.priority)) {
239
+ return `${label}: pick the task’s priority`;
240
+ }
241
+ if (!Number.isInteger(step.dueInDays) || step.dueInDays < 0 || step.dueInDays > CRM_TASK_MAX_DUE_DAYS) {
242
+ return `${label}: due in 0–${CRM_TASK_MAX_DUE_DAYS} days`;
243
+ }
244
+ // Optional — but an address that is not one would resolve to nobody at
245
+ // run time, and the task would land unassigned with no word why.
246
+ const assigneeEmail = (_ref3 = (_step_assigneeEmail = step.assigneeEmail) == null ? void 0 : _step_assigneeEmail.trim()) != null ? _ref3 : '';
247
+ if (assigneeEmail && !assigneeEmail.includes('@')) {
248
+ return `${label}: enter the assignee’s email address`;
249
+ }
250
+ }
251
+ if (step.type === 'logCrmActivity') {
252
+ var _step_body1;
253
+ if (!isCrmActivityKind(step.kind)) {
254
+ return `${label}: pick the kind of activity`;
255
+ }
256
+ if (step.direction !== undefined) {
257
+ var _ACTIVITY_DIRECTIONS_step_kind;
258
+ const directions = (_ACTIVITY_DIRECTIONS_step_kind = ACTIVITY_DIRECTIONS[step.kind]) != null ? _ACTIVITY_DIRECTIONS_step_kind : [];
259
+ if (!directions.includes(step.direction)) {
260
+ return directions.length ? `${label}: pick which way the ${step.kind} went` : `${label}: only a call or an email takes a direction`;
261
+ }
262
+ }
263
+ if (!((_step_body1 = step.body) == null ? void 0 : _step_body1.trim())) return `${label}: write what happened`;
264
+ }
265
+ return null;
266
+ }
267
+
268
+ //# sourceMappingURL=host-actions.js.map