@aglyn/plugins-workflows 1.0.0-beta.199 → 1.0.0-beta.200

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.
@@ -15,8 +15,10 @@ 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 { ACTION_MAX_EVENT_DEPTH, checkEntitlement, evaluateExpression, FLOW_TIMED_OUT_FIELD, resolveOrgEntitlements, runWorkflow } from "@aglyn/aglyn/server";
18
+ import { activitySearchTokens } from "@aglyn/aglyn/app-utils/activity-search";
18
19
  import { firebaseAdmin, getOrgForHost } from "@aglyn/tenant-data-admin";
19
20
  import { FieldValue } from "firebase-admin/firestore";
21
+ import { runSummaryFields } from "../model/run-history.js";
20
22
  import { deferFlowEnrollment, endFlowEnrollment } from "./flow-enrollments.js";
21
23
  import { automationRunEnv, executeWorkflow, FLOW_CLAIM_RETRY_MS, stopFlowEnrollment } from "./run-event-actions.js";
22
24
  import { workflowHasActionSteps } from "./workflow-steps.js";
@@ -31,6 +33,10 @@ import { workflowHasActionSteps } from "./workflow-steps.js";
31
33
  actorId: null,
32
34
  actorEmail: null
33
35
  }, row, {
36
+ // The site log's search finds a run by its workflow's name (AGL-3321).
37
+ searchTokens: activitySearchTokens({
38
+ target: row['target']
39
+ }),
34
40
  createdAt: FieldValue.serverTimestamp()
35
41
  })).catch(()=>undefined);
36
42
  }
@@ -43,11 +49,11 @@ import { workflowHasActionSteps } from "./workflow-steps.js";
43
49
  */ export function workflowRunRow(execution, input) {
44
50
  const failed = execution.errors.length > 0;
45
51
  const action = failed ? `Workflow ran on ${input.event} with errors: ${execution.errors.join('; ')}`.slice(0, 300) : execution.ending === 'waiting' ? `Workflow is waiting, on ${input.event}` : `Workflow ran on ${input.event}`;
46
- return {
52
+ return _extends({
47
53
  action,
48
54
  result: failed ? 'failed' : 'succeeded',
49
- trigger: input.event,
50
- summary: (execution.outcomes.length ? execution.outcomes.join(' · ') : 'Ran').slice(0, 300),
55
+ trigger: input.event
56
+ }, runSummaryFields((execution.outcomes.length ? execution.outcomes.join(' · ') : 'Ran').slice(0, 300)), {
51
57
  status: failed ? 'error' : 'ok',
52
58
  durationMs: input.durationMs,
53
59
  target: {
@@ -55,7 +61,7 @@ import { workflowHasActionSteps } from "./workflow-steps.js";
55
61
  id: input.workflowId,
56
62
  name: input.workflowName
57
63
  }
58
- };
64
+ });
59
65
  }
60
66
  /**
61
67
  * Event-triggered workflow runner (AGL-128): loads workflows whose
@@ -214,11 +220,11 @@ import { workflowHasActionSteps } from "./workflow-steps.js";
214
220
  *
215
221
  * `status` stays: the dashboard activity feed renders it, and removing
216
222
  * it would trade one silent gap for another.
217
- */ await recordWorkflowRun(hostRef, {
223
+ */ await recordWorkflowRun(hostRef, _extends({
218
224
  action,
219
225
  result: run.ok === false ? 'failed' : 'succeeded',
220
- trigger: event,
221
- summary: run.ok === false ? String(run.error).slice(0, 300) : 'Ran',
226
+ trigger: event
227
+ }, runSummaryFields(run.ok === false ? String(run.error).slice(0, 300) : 'Ran'), {
222
228
  status: run.ok === false ? 'error' : 'ok',
223
229
  durationMs,
224
230
  target: {
@@ -226,7 +232,7 @@ import { workflowHasActionSteps } from "./workflow-steps.js";
226
232
  id: doc.id,
227
233
  name: (_workflow_name = workflow.name) != null ? _workflow_name : ''
228
234
  }
229
- });
235
+ }));
230
236
  }
231
237
  // Filtered-out workflows do not bill — only executed runs count.
232
238
  if (executed > 0) {
@@ -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 { firebaseAdmin, getOrgForHost } from '@aglyn/tenant-data-admin'\nimport type { HostEventPayload } from '@aglyn/tenant-runtime/host-event-listeners'\nimport { FieldValue } from 'firebase-admin/firestore'\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'\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 ...row,\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 summary: (execution.outcomes.length\n ? execution.outcomes.join(' · ')\n : 'Ran'\n ).slice(0, 300),\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 {\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 summary: run.ok === false ? String(run.error).slice(0, 300) : 'Ran',\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","firebaseAdmin","getOrgForHost","FieldValue","deferFlowEnrollment","endFlowEnrollment","automationRunEnv","executeWorkflow","FLOW_CLAIM_RETRY_MS","stopFlowEnrollment","workflowHasActionSteps","MAX_TRIGGERED_WORKFLOWS","recordWorkflowRun","hostRef","row","collection","add","actorId","actorEmail","createdAt","serverTimestamp","catch","undefined","workflowRunRow","execution","input","failed","errors","length","action","event","join","slice","ending","result","trigger","summary","outcomes","status","durationMs","target","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","counterSnapshot","org","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 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 {\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'\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 ...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 {\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","deferFlowEnrollment","endFlowEnrollment","automationRunEnv","executeWorkflow","FLOW_CLAIM_RETRY_MS","stopFlowEnrollment","workflowHasActionSteps","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","counterSnapshot","org","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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"}
@@ -0,0 +1,79 @@
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 { ListFilterClause } from '@aglyn/shared-ui-jsx/const/list-grid-filter';
18
+ import type { ListQueryDeclaration, ListQueryFilter } from '@aglyn/shared-ui-jsx/const/list-query-plan';
19
+ /** A run's verdict, as every run writer stores it. */
20
+ export declare const RUN_RESULTS: readonly ["succeeded", "failed", "skipped"];
21
+ export type RunResultValue = (typeof RUN_RESULTS)[number];
22
+ /** The written word-prefix tokens of a run's summary, for the search box. */
23
+ export declare const RUN_SUMMARY_TOKENS = "summaryTokens";
24
+ /**
25
+ * What a run writer spreads beside `summary`: the summary itself and its
26
+ * search tokens, so the two cannot be written apart.
27
+ */
28
+ export declare function runSummaryFields(summary: string): {
29
+ summary: string;
30
+ summaryTokens: string[];
31
+ };
32
+ /**
33
+ * What the Filters panel offers, each a predicate the query serves beneath
34
+ * its newest-first order: Time is that order's own field, so its ranges cost
35
+ * no index of their own; Trigger and Result are equalities.
36
+ *
37
+ * `What happened` has no column filter: its words are the search box's,
38
+ * which reads the same tokens, and a `contains` beside the search would be a
39
+ * second array clause the query cannot hold.
40
+ */
41
+ export declare const RUN_HISTORY_QUERY: ListQueryDeclaration;
42
+ export declare const RUN_HISTORY_HEADERS: Readonly<Record<string, string>>;
43
+ /**
44
+ * The history's scope: this automation's entries, and only runs.
45
+ *
46
+ * "Only runs" is `result in [all three]` — unless the reader has asked for
47
+ * a Result, whose own equality already names nothing but runs. The two are
48
+ * never both on the query: a second predicate on the same field would only
49
+ * repeat the first, and the plan serves an `equals` or `isAnyOf` on Result
50
+ * unconditionally, so dropping the scope never lets a non-run through.
51
+ */
52
+ export declare function runHistoryBase(targetId: string | undefined, clauses: readonly ListFilterClause[]): ListQueryFilter[];
53
+ /** The base's shape, for the index enumeration. */
54
+ export declare const RUN_HISTORY_BASE_PATHS: readonly [{
55
+ readonly path: "target.id";
56
+ }, {
57
+ readonly path: "result";
58
+ }];
59
+ /** A run entry as stored, as far as completing it is concerned. */
60
+ export interface RunEntryLike {
61
+ action?: unknown;
62
+ result?: unknown;
63
+ trigger?: unknown;
64
+ summary?: unknown;
65
+ summaryTokens?: unknown;
66
+ }
67
+ /**
68
+ * What a stored activity entry lacks to be found by the run history's query,
69
+ * or `null` when it is not a run or already complete. The definition the
70
+ * backfill restates (`tools/scripts/backfill-activity-run-fields.mjs`), held
71
+ * to the same worked examples (`tools/scripts/lib/activity-run-fields.fixtures.json`).
72
+ *
73
+ * A run recorded before its structured fields carried only the prose line
74
+ * `Action ran on <event>` — with ` with errors: <what failed>` when it
75
+ * failed — so the verdict, the event and the summary are read back from it,
76
+ * the way the history has always shown such a row. An entry that is neither
77
+ * a stored verdict nor that prose is not a run and is left alone.
78
+ */
79
+ export declare function runEntryCompletion(entry: RunEntryLike): Record<string, unknown> | null;
@@ -0,0 +1,180 @@
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 { nameSearchTokens } from "@aglyn/aglyn/app-utils/name-search";
17
+ /*
18
+ * THE RUN HISTORY, AS A QUERY (AGL-3321).
19
+ *
20
+ * A run is an entry in `hosts/{hostId}/activity` that carries a verdict:
21
+ * `result` is `succeeded`, `failed` or `skipped`, beside the `trigger` event
22
+ * and the `summary` of what each step did. The activity feed also holds
23
+ * publishes, media saves and member changes, so the history's query is
24
+ * scoped to the automation (`target.id`) AND to runs (`result`) — and every
25
+ * clause the Filters panel accepts, and the search word, is another
26
+ * predicate on that same query. Pages come from the query, so a page of ten
27
+ * is ten runs and a filtered page is ten matches, however far back they sit.
28
+ *
29
+ * Every run writer stamps `summaryTokens` beside the summary
30
+ * ({@link runSummaryFields}), and `tools/scripts/backfill-activity-run-fields.mjs`
31
+ * completes the entries written before: `result`, `trigger` and `summary`
32
+ * from the prose line of a run recorded before those fields existed, and the
33
+ * tokens on every run. A run without a `result` is invisible to the query,
34
+ * which is why the backfill is part of shipping this.
35
+ */ /** A run's verdict, as every run writer stores it. */ export const RUN_RESULTS = [
36
+ 'succeeded',
37
+ 'failed',
38
+ 'skipped'
39
+ ];
40
+ /** The written word-prefix tokens of a run's summary, for the search box. */ export const RUN_SUMMARY_TOKENS = 'summaryTokens';
41
+ /**
42
+ * What a run writer spreads beside `summary`: the summary itself and its
43
+ * search tokens, so the two cannot be written apart.
44
+ */ export function runSummaryFields(summary) {
45
+ return {
46
+ summary,
47
+ summaryTokens: nameSearchTokens(summary)
48
+ };
49
+ }
50
+ /**
51
+ * What the Filters panel offers, each a predicate the query serves beneath
52
+ * its newest-first order: Time is that order's own field, so its ranges cost
53
+ * no index of their own; Trigger and Result are equalities.
54
+ *
55
+ * `What happened` has no column filter: its words are the search box's,
56
+ * which reads the same tokens, and a `contains` beside the search would be a
57
+ * second array clause the query cannot hold.
58
+ */ export const RUN_HISTORY_QUERY = {
59
+ fields: [
60
+ {
61
+ column: 'createdAtMs',
62
+ kind: 'date',
63
+ path: 'createdAt',
64
+ operators: [
65
+ 'is',
66
+ 'after',
67
+ 'onOrAfter',
68
+ 'before',
69
+ 'onOrBefore'
70
+ ]
71
+ },
72
+ {
73
+ column: 'trigger',
74
+ kind: 'exact',
75
+ path: 'trigger',
76
+ operators: [
77
+ 'equals',
78
+ 'isAnyOf'
79
+ ]
80
+ },
81
+ {
82
+ column: 'result',
83
+ kind: 'exact',
84
+ path: 'result',
85
+ operators: [
86
+ 'equals',
87
+ 'isAnyOf'
88
+ ]
89
+ }
90
+ ],
91
+ sorts: [
92
+ {
93
+ path: 'createdAt',
94
+ direction: 'desc',
95
+ column: 'createdAtMs'
96
+ }
97
+ ],
98
+ search: {
99
+ tokensPath: RUN_SUMMARY_TOKENS
100
+ }
101
+ };
102
+ export const RUN_HISTORY_HEADERS = {
103
+ createdAtMs: 'Time',
104
+ trigger: 'Trigger',
105
+ result: 'Result'
106
+ };
107
+ /** A clause on Result the query serves — which is then what keeps it to runs. */ const namesResult = (clause)=>clause.field === 'result' && (clause.op === 'equals' || clause.op === 'isAnyOf') && clause.value.trim() !== '';
108
+ /**
109
+ * The history's scope: this automation's entries, and only runs.
110
+ *
111
+ * "Only runs" is `result in [all three]` — unless the reader has asked for
112
+ * a Result, whose own equality already names nothing but runs. The two are
113
+ * never both on the query: a second predicate on the same field would only
114
+ * repeat the first, and the plan serves an `equals` or `isAnyOf` on Result
115
+ * unconditionally, so dropping the scope never lets a non-run through.
116
+ */ export function runHistoryBase(targetId, clauses) {
117
+ const base = [];
118
+ if (targetId) base.push({
119
+ path: 'target.id',
120
+ op: '==',
121
+ value: targetId
122
+ });
123
+ if (!clauses.some(namesResult)) base.push({
124
+ path: 'result',
125
+ op: 'in',
126
+ value: [
127
+ ...RUN_RESULTS
128
+ ]
129
+ });
130
+ return base;
131
+ }
132
+ /** The base's shape, for the index enumeration. */ export const RUN_HISTORY_BASE_PATHS = [
133
+ {
134
+ path: 'target.id'
135
+ },
136
+ {
137
+ path: 'result'
138
+ }
139
+ ];
140
+ /**
141
+ * What a stored activity entry lacks to be found by the run history's query,
142
+ * or `null` when it is not a run or already complete. The definition the
143
+ * backfill restates (`tools/scripts/backfill-activity-run-fields.mjs`), held
144
+ * to the same worked examples (`tools/scripts/lib/activity-run-fields.fixtures.json`).
145
+ *
146
+ * A run recorded before its structured fields carried only the prose line
147
+ * `Action ran on <event>` — with ` with errors: <what failed>` when it
148
+ * failed — so the verdict, the event and the summary are read back from it,
149
+ * the way the history has always shown such a row. An entry that is neither
150
+ * a stored verdict nor that prose is not a run and is left alone.
151
+ */ export function runEntryCompletion(entry) {
152
+ var _entry_result, _entry_action;
153
+ const stored = String((_entry_result = entry.result) != null ? _entry_result : '').trim();
154
+ const action = String((_entry_action = entry.action) != null ? _entry_action : '').trim();
155
+ const legacy = action.startsWith('Action ran on');
156
+ if (!RUN_RESULTS.includes(stored) && !legacy) return null;
157
+ const patch = {};
158
+ if (!RUN_RESULTS.includes(stored)) {
159
+ patch['result'] = action.includes('with errors:') ? 'failed' : 'succeeded';
160
+ }
161
+ if (typeof entry.trigger !== 'string' || !entry.trigger) {
162
+ var _exec;
163
+ const event = legacy ? (_exec = /^Action ran on (\S+)/.exec(action)) == null ? void 0 : _exec[1] : undefined;
164
+ if (event) patch['trigger'] = event;
165
+ }
166
+ let summary = typeof entry.summary === 'string' ? entry.summary.trim() : '';
167
+ if (!summary) {
168
+ const errors = /with errors:\s*(.+)$/.exec(action);
169
+ summary = errors ? errors[1] : legacy ? 'Ran' : action;
170
+ patch['summary'] = summary;
171
+ }
172
+ const tokens = nameSearchTokens(summary);
173
+ const held = Array.isArray(entry.summaryTokens) ? entry.summaryTokens : null;
174
+ if (!held || held.length !== tokens.length || held.some((token, at)=>token !== tokens[at])) {
175
+ patch['summaryTokens'] = tokens;
176
+ }
177
+ return Object.keys(patch).length ? patch : null;
178
+ }
179
+
180
+ //# sourceMappingURL=run-history.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/workflows/src/lib/model/run-history.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 { nameSearchTokens } from '@aglyn/aglyn/app-utils/name-search'\nimport type { ListFilterField } from '@aglyn/shared-ui-jsx/const/list-filter'\nimport type { ListFilterClause } from '@aglyn/shared-ui-jsx/const/list-grid-filter'\nimport type {\n ListQueryDeclaration,\n ListQueryFilter,\n} from '@aglyn/shared-ui-jsx/const/list-query-plan'\n\n/*\n * THE RUN HISTORY, AS A QUERY (AGL-3321).\n *\n * A run is an entry in `hosts/{hostId}/activity` that carries a verdict:\n * `result` is `succeeded`, `failed` or `skipped`, beside the `trigger` event\n * and the `summary` of what each step did. The activity feed also holds\n * publishes, media saves and member changes, so the history's query is\n * scoped to the automation (`target.id`) AND to runs (`result`) — and every\n * clause the Filters panel accepts, and the search word, is another\n * predicate on that same query. Pages come from the query, so a page of ten\n * is ten runs and a filtered page is ten matches, however far back they sit.\n *\n * Every run writer stamps `summaryTokens` beside the summary\n * ({@link runSummaryFields}), and `tools/scripts/backfill-activity-run-fields.mjs`\n * completes the entries written before: `result`, `trigger` and `summary`\n * from the prose line of a run recorded before those fields existed, and the\n * tokens on every run. A run without a `result` is invisible to the query,\n * which is why the backfill is part of shipping this.\n */\n\n/** A run's verdict, as every run writer stores it. */\nexport const RUN_RESULTS = ['succeeded', 'failed', 'skipped'] as const\nexport type RunResultValue = (typeof RUN_RESULTS)[number]\n\n/** The written word-prefix tokens of a run's summary, for the search box. */\nexport const RUN_SUMMARY_TOKENS = 'summaryTokens'\n\n/**\n * What a run writer spreads beside `summary`: the summary itself and its\n * search tokens, so the two cannot be written apart.\n */\nexport function runSummaryFields(summary: string): {\n summary: string\n summaryTokens: string[]\n} {\n return { summary, summaryTokens: nameSearchTokens(summary) }\n}\n\n/**\n * What the Filters panel offers, each a predicate the query serves beneath\n * its newest-first order: Time is that order's own field, so its ranges cost\n * no index of their own; Trigger and Result are equalities.\n *\n * `What happened` has no column filter: its words are the search box's,\n * which reads the same tokens, and a `contains` beside the search would be a\n * second array clause the query cannot hold.\n */\nexport const RUN_HISTORY_QUERY: ListQueryDeclaration = {\n fields: [\n {\n column: 'createdAtMs',\n kind: 'date',\n path: 'createdAt',\n operators: ['is', 'after', 'onOrAfter', 'before', 'onOrBefore'],\n },\n {\n column: 'trigger',\n kind: 'exact',\n path: 'trigger',\n operators: ['equals', 'isAnyOf'],\n },\n {\n column: 'result',\n kind: 'exact',\n path: 'result',\n operators: ['equals', 'isAnyOf'],\n },\n ] satisfies ListFilterField[],\n sorts: [{ path: 'createdAt', direction: 'desc', column: 'createdAtMs' }],\n search: { tokensPath: RUN_SUMMARY_TOKENS },\n}\n\nexport const RUN_HISTORY_HEADERS: Readonly<Record<string, string>> = {\n createdAtMs: 'Time',\n trigger: 'Trigger',\n result: 'Result',\n}\n\n/** A clause on Result the query serves — which is then what keeps it to runs. */\nconst namesResult = (clause: ListFilterClause): boolean =>\n clause.field === 'result' &&\n (clause.op === 'equals' || clause.op === 'isAnyOf') &&\n clause.value.trim() !== ''\n\n/**\n * The history's scope: this automation's entries, and only runs.\n *\n * \"Only runs\" is `result in [all three]` — unless the reader has asked for\n * a Result, whose own equality already names nothing but runs. The two are\n * never both on the query: a second predicate on the same field would only\n * repeat the first, and the plan serves an `equals` or `isAnyOf` on Result\n * unconditionally, so dropping the scope never lets a non-run through.\n */\nexport function runHistoryBase(\n targetId: string | undefined,\n clauses: readonly ListFilterClause[],\n): ListQueryFilter[] {\n const base: ListQueryFilter[] = []\n if (targetId) base.push({ path: 'target.id', op: '==', value: targetId })\n if (!clauses.some(namesResult))\n base.push({ path: 'result', op: 'in', value: [...RUN_RESULTS] })\n return base\n}\n\n/** The base's shape, for the index enumeration. */\nexport const RUN_HISTORY_BASE_PATHS = [\n { path: 'target.id' },\n { path: 'result' },\n] as const\n\n/** A run entry as stored, as far as completing it is concerned. */\nexport interface RunEntryLike {\n action?: unknown\n result?: unknown\n trigger?: unknown\n summary?: unknown\n summaryTokens?: unknown\n}\n\n/**\n * What a stored activity entry lacks to be found by the run history's query,\n * or `null` when it is not a run or already complete. The definition the\n * backfill restates (`tools/scripts/backfill-activity-run-fields.mjs`), held\n * to the same worked examples (`tools/scripts/lib/activity-run-fields.fixtures.json`).\n *\n * A run recorded before its structured fields carried only the prose line\n * `Action ran on <event>` — with ` with errors: <what failed>` when it\n * failed — so the verdict, the event and the summary are read back from it,\n * the way the history has always shown such a row. An entry that is neither\n * a stored verdict nor that prose is not a run and is left alone.\n */\nexport function runEntryCompletion(\n entry: RunEntryLike,\n): Record<string, unknown> | null {\n const stored = String(entry.result ?? '').trim()\n const action = String(entry.action ?? '').trim()\n const legacy = action.startsWith('Action ran on')\n if (!(RUN_RESULTS as readonly string[]).includes(stored) && !legacy)\n return null\n const patch: Record<string, unknown> = {}\n if (!(RUN_RESULTS as readonly string[]).includes(stored)) {\n patch['result'] = action.includes('with errors:') ? 'failed' : 'succeeded'\n }\n if (typeof entry.trigger !== 'string' || !entry.trigger) {\n const event = legacy ? /^Action ran on (\\S+)/.exec(action)?.[1] : undefined\n if (event) patch['trigger'] = event\n }\n let summary = typeof entry.summary === 'string' ? entry.summary.trim() : ''\n if (!summary) {\n const errors = /with errors:\\s*(.+)$/.exec(action)\n summary = errors ? errors[1] : legacy ? 'Ran' : action\n patch['summary'] = summary\n }\n const tokens = nameSearchTokens(summary)\n const held = Array.isArray(entry.summaryTokens) ? entry.summaryTokens : null\n if (\n !held ||\n held.length !== tokens.length ||\n held.some((token, at) => token !== tokens[at])\n ) {\n patch['summaryTokens'] = tokens\n }\n return Object.keys(patch).length ? patch : null\n}\n"],"names":["nameSearchTokens","RUN_RESULTS","RUN_SUMMARY_TOKENS","runSummaryFields","summary","summaryTokens","RUN_HISTORY_QUERY","fields","column","kind","path","operators","sorts","direction","search","tokensPath","RUN_HISTORY_HEADERS","createdAtMs","trigger","result","namesResult","clause","field","op","value","trim","runHistoryBase","targetId","clauses","base","push","some","RUN_HISTORY_BASE_PATHS","runEntryCompletion","entry","stored","String","action","legacy","startsWith","includes","patch","event","exec","undefined","errors","tokens","held","Array","isArray","length","token","at","Object","keys"],"mappings":"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"}
package/src/lib/server.js CHANGED
@@ -16,9 +16,11 @@ import { _ as _extends } from "@swc/helpers/_/_extends";
16
16
  * limitations under the License.
17
17
  */ import { checkEntitlement, resolveOrgEntitlements, runWorkflow } from "@aglyn/aglyn/server";
18
18
  import { registerPluginApiRoute, registerPluginJob } from "@aglyn/aglyn/server";
19
+ import { activitySearchTokens } from "@aglyn/aglyn/app-utils/activity-search";
19
20
  import { firebaseAdmin, getOrgForHost } from "@aglyn/tenant-data-admin";
20
21
  import { timingSafeEqual } from "crypto";
21
22
  import { FieldValue } from "firebase-admin/firestore";
23
+ import { runSummaryFields } from "./model/run-history.js";
22
24
  import { BUNDLE_ID as WORKFLOWS_BUNDLE_ID } from "./constants/bundle-common.js";
23
25
  import { registerWorkflowsServerDeclarations } from "./declarations.server.js";
24
26
  import { automationRunEnv, executeWorkflow, runDueFlowEnrollments, workflowContextFromDocs } from "./engine/run-event-actions.js";
@@ -241,22 +243,28 @@ function secretsMatch(expected, provided) {
241
243
  };
242
244
  })();
243
245
  const failed = outcome.error !== null;
244
- await hostRef.collection('activity').add({
246
+ await hostRef.collection('activity').add(_extends({
245
247
  actorId: null,
246
248
  actorEmail: null,
247
249
  action: failed ? `Inbound webhook run failed: ${outcome.error}`.slice(0, 300) : `Inbound webhook ran "${hook.workflowName}"`,
248
250
  // The run-history shape (AGL-2222) — without `result` this execution
249
251
  // is invisible to the Runs table, which reads a verdict and not prose.
250
252
  result: failed ? 'failed' : 'succeeded',
251
- trigger: hookEvent,
252
- summary: failed ? String(outcome.error).slice(0, 300) : outcome.summary.slice(0, 300),
253
+ trigger: hookEvent
254
+ }, runSummaryFields(failed ? String(outcome.error).slice(0, 300) : outcome.summary.slice(0, 300)), {
253
255
  target: {
254
256
  type: 'workflow',
255
257
  id: hookId,
256
258
  name: (_hook_name1 = hook.name) != null ? _hook_name1 : ''
257
259
  },
260
+ // The site log's search finds a run by its hook's name (AGL-3321).
261
+ searchTokens: activitySearchTokens({
262
+ target: {
263
+ name: hook.name
264
+ }
265
+ }),
258
266
  createdAt: FieldValue.serverTimestamp()
259
- }).catch(()=>undefined);
267
+ })).catch(()=>undefined);
260
268
  // The run happened, so it counts — including a FAILED one, exactly as
261
269
  // `runEventWorkflows` counts its failures. A run that executed and threw
262
270
  // spent the same compute as one that returned a value; only a run that