From 2b56b54c0b70df2f274777659277e4d765715dcb Mon Sep 17 00:00:00 2001 From: alban bertolini Date: Sun, 21 Jun 2026 14:35:54 +0200 Subject: [PATCH] feat(workflow-executor): audit trail for ai-filled and ai-submitted action forms (PRD-513) Surfaces the form-fill audit data already persisted by AI-assisted/Full AI: - record submittedBy ('ai' = Full AI executor, 'user' = AI-assisted native submit) on the trigger-action executionResult - new step-summary formatter for trigger-action: distinguishes an executed action from one only submitted for approval (downstream AI must NOT treat a pending-approval action as executed), names the submitter, lists the AI pre-filled fields and the fields the human edited (diff prefill vs submitted) fixes PRD-513 Co-Authored-By: Claude Opus 4.8 (1M context) --- .../summary/step-execution-formatters.ts | 43 +++++++++++ .../trigger-record-action-step-executor.ts | 3 + .../src/types/step-execution-data.ts | 3 + .../step-execution-formatters.test.ts | 76 +++++++++++++++++++ ...rigger-record-action-step-executor.test.ts | 9 ++- 5 files changed, 133 insertions(+), 1 deletion(-) diff --git a/packages/workflow-executor/src/executors/summary/step-execution-formatters.ts b/packages/workflow-executor/src/executors/summary/step-execution-formatters.ts index c9df62c0a8..60eab1f29b 100644 --- a/packages/workflow-executor/src/executors/summary/step-execution-formatters.ts +++ b/packages/workflow-executor/src/executors/summary/step-execution-formatters.ts @@ -3,6 +3,7 @@ import type { LoadRelatedRecordStepExecutionData, McpStepExecutionData, StepExecutionData, + TriggerRecordActionStepExecutionData, } from '../../types/step-execution-data'; export default class StepExecutionFormatters { @@ -16,11 +17,53 @@ export default class StepExecutionFormatters { return StepExecutionFormatters.formatMcp(execution as McpStepExecutionData); case 'guidance': return StepExecutionFormatters.formatGuidance(execution as GuidanceStepExecutionData); + case 'trigger-action': + return StepExecutionFormatters.formatTriggerAction( + execution as TriggerRecordActionStepExecutionData, + ); default: return null; } } + // Audit/context summary for a Trigger Action (PRD-513). Critically distinguishes an executed + // action from one merely submitted for approval — downstream AI steps must NOT treat a + // pending-approval action as if it ran. + private static formatTriggerAction( + execution: TriggerRecordActionStepExecutionData, + ): string | null { + const { executionResult } = execution; + if (!executionResult || 'skipped' in executionResult) return null; + + const action = execution.executionParams?.displayName ?? 'the action'; + const submitter = executionResult.submittedBy === 'ai' ? 'AI' : 'the user'; + + if (executionResult.submissionOutcome === 'pending-approval') { + return ` Submitted action "${action}" for approval (by ${submitter}). It is AWAITING APPROVAL and has NOT been executed — no result is available yet.`; + } + + const lines = [` Triggered action "${action}" (submitted by ${submitter}).`]; + const aiFilled = executionResult.aiFilledValues; + + if (aiFilled?.length) { + lines.push(` AI pre-filled: ${aiFilled.map(v => v.field).join(', ')}.`); + + // Human-edited fields = those whose submitted value differs from the AI prefill (AI-assisted). + const submitted = executionResult.submittedValues; + + if (submitted) { + const aiMap = Object.fromEntries(aiFilled.map(v => [v.field, v.value])); + const edited = Object.keys(submitted).filter( + field => JSON.stringify(submitted[field]) !== JSON.stringify(aiMap[field]), + ); + + if (edited.length) lines.push(` Edited by the user before submitting: ${edited.join(', ')}.`); + } + } + + return lines.join('\n'); + } + private static formatMcp(execution: McpStepExecutionData): string | null { const { executionResult } = execution; if (!executionResult) return null; diff --git a/packages/workflow-executor/src/executors/trigger-record-action-step-executor.ts b/packages/workflow-executor/src/executors/trigger-record-action-step-executor.ts index 4e2f114939..461c0d4010 100644 --- a/packages/workflow-executor/src/executors/trigger-record-action-step-executor.ts +++ b/packages/workflow-executor/src/executors/trigger-record-action-step-executor.ts @@ -372,6 +372,7 @@ export default class TriggerRecordActionStepExecutor extends RecordStepExecutor< // Form-bearing Full AI: record what the executor submitted (PRD-512/513). ...(form && { submissionOutcome: 'executed', + submittedBy: 'ai', submittedValues: form.values, ...(form.aiFilledValues.length && { aiFilledValues: form.aiFilledValues }), }), @@ -407,6 +408,8 @@ export default class TriggerRecordActionStepExecutor extends RecordStepExecutor< // No action result exists yet when the submission only created an approval request. ...(submissionOutcome === 'executed' && { actionResult: confirmation?.actionResult }), submissionOutcome, + // AI-assisted = the human submitted natively (PRD-513 audit). + submittedBy: 'user', ...(confirmation?.submittedValues && { submittedValues: confirmation.submittedValues }), ...(aiFilledValues?.length && { aiFilledValues }), }, diff --git a/packages/workflow-executor/src/types/step-execution-data.ts b/packages/workflow-executor/src/types/step-execution-data.ts index 2fc2e2f310..7700d3ecd3 100644 --- a/packages/workflow-executor/src/types/step-execution-data.ts +++ b/packages/workflow-executor/src/types/step-execution-data.ts @@ -123,6 +123,9 @@ export interface TriggerRecordActionStepExecutionData // Final values the front submitted + the ordered AI prefill — PRD-513 audit (human-edit diff). submittedValues?: Record; aiFilledValues?: AiFilledFormValue[]; + // Who submitted the action (PRD-513 audit): 'ai' = Full AI (executor), 'user' = AI-assisted + // (human via the native front). Absent for formless/legacy flows. + submittedBy?: 'ai' | 'user'; } | { skipped: true }; pendingData?: TriggerActionPendingData; diff --git a/packages/workflow-executor/test/executors/step-execution-formatters.test.ts b/packages/workflow-executor/test/executors/step-execution-formatters.test.ts index a11f34029f..9cce29b12e 100644 --- a/packages/workflow-executor/test/executors/step-execution-formatters.test.ts +++ b/packages/workflow-executor/test/executors/step-execution-formatters.test.ts @@ -168,5 +168,81 @@ describe('StepExecutionFormatters', () => { expect(StepExecutionFormatters.format(execution)).toBeNull(); }); }); + + describe('trigger-action (PRD-513)', () => { + const recordRef = { collectionName: 'customers', recordId: [42], stepIndex: 0 }; + + it('marks a pending-approval submission as NOT executed', () => { + const execution: StepExecutionData = { + type: 'trigger-action', + stepIndex: 1, + selectedRecordRef: recordRef, + executionParams: { name: 'refund', displayName: 'Process refund' }, + executionResult: { + success: true, + submissionOutcome: 'pending-approval', + submittedBy: 'user', + }, + }; + + const summary = StepExecutionFormatters.format(execution); + expect(summary).toContain('AWAITING APPROVAL'); + expect(summary).toContain('has NOT been executed'); + }); + + it('reports a Full AI execution as submitted by AI with the pre-filled fields', () => { + const execution: StepExecutionData = { + type: 'trigger-action', + stepIndex: 1, + selectedRecordRef: recordRef, + executionParams: { name: 'refund', displayName: 'Process refund' }, + executionResult: { + success: true, + actionResult: { ok: true }, + submissionOutcome: 'executed', + submittedBy: 'ai', + submittedValues: { amount: 50 }, + aiFilledValues: [{ field: 'amount', value: 50 }], + }, + }; + + const summary = StepExecutionFormatters.format(execution) ?? ''; + expect(summary).toContain('submitted by AI'); + expect(summary).toContain('AI pre-filled: amount'); + }); + + it('reports human edits in AI-assisted mode (diff prefill vs submitted)', () => { + const execution: StepExecutionData = { + type: 'trigger-action', + stepIndex: 1, + selectedRecordRef: recordRef, + executionParams: { name: 'refund', displayName: 'Process refund' }, + executionResult: { + success: true, + actionResult: { ok: true }, + submissionOutcome: 'executed', + submittedBy: 'user', + // AI proposed amount 50; the human changed it to 80 before submitting. + aiFilledValues: [{ field: 'amount', value: 50 }], + submittedValues: { amount: 80 }, + }, + }; + + const summary = StepExecutionFormatters.format(execution) ?? ''; + expect(summary).toContain('submitted by the user'); + expect(summary).toContain('Edited by the user before submitting: amount'); + }); + + it('returns null for a skipped action', () => { + const execution: StepExecutionData = { + type: 'trigger-action', + stepIndex: 1, + selectedRecordRef: recordRef, + executionResult: { skipped: true }, + }; + + expect(StepExecutionFormatters.format(execution)).toBeNull(); + }); + }); }); }); diff --git a/packages/workflow-executor/test/executors/trigger-record-action-step-executor.test.ts b/packages/workflow-executor/test/executors/trigger-record-action-step-executor.test.ts index 31fa539379..ab7a90fe2c 100644 --- a/packages/workflow-executor/test/executors/trigger-record-action-step-executor.test.ts +++ b/packages/workflow-executor/test/executors/trigger-record-action-step-executor.test.ts @@ -460,6 +460,7 @@ describe('TriggerRecordActionStepExecutor', () => { success: true, actionResult: { success: 'ok', html: '

Email queued

' }, submissionOutcome: 'executed', + submittedBy: 'user', }, pendingData: { displayName: 'Send Welcome Email', @@ -497,7 +498,12 @@ describe('TriggerRecordActionStepExecutor', () => { expect(runStore.saveStepExecution).toHaveBeenCalledWith( 'run-1', expect.objectContaining({ - executionResult: { success: true, actionResult: null, submissionOutcome: 'executed' }, + executionResult: { + success: true, + actionResult: null, + submissionOutcome: 'executed', + submittedBy: 'user', + }, }), ); }); @@ -1535,6 +1541,7 @@ describe('TriggerRecordActionStepExecutor', () => { executionResult: { success: true, submissionOutcome: 'pending-approval', + submittedBy: 'user', submittedValues: { amount: 50 }, aiFilledValues: [{ field: 'amount', value: 50 }], },