Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
LoadRelatedRecordStepExecutionData,
McpStepExecutionData,
StepExecutionData,
TriggerRecordActionStepExecutionData,
} from '../../types/step-execution-data';

export default class StepExecutionFormatters {
Expand All @@ -16,11 +17,53 @@
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(', ')}.`);

Check failure on line 60 in packages/workflow-executor/src/executors/summary/step-execution-formatters.ts

View workflow job for this annotation

GitHub Actions / Linting & Testing (workflow-executor)

Insert `⏎·········`
}
}

return lines.join('\n');
}

private static formatMcp(execution: McpStepExecutionData): string | null {
const { executionResult } = execution;
if (!executionResult) return null;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 }),
}),
Expand Down Expand Up @@ -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 }),
},
Expand Down
3 changes: 3 additions & 0 deletions packages/workflow-executor/src/types/step-execution-data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,9 @@ export interface TriggerRecordActionStepExecutionData
// Final values the front submitted + the ordered AI prefill — PRD-513 audit (human-edit diff).
submittedValues?: Record<string, unknown>;
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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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();
});
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -460,6 +460,7 @@ describe('TriggerRecordActionStepExecutor', () => {
success: true,
actionResult: { success: 'ok', html: '<p>Email queued</p>' },
submissionOutcome: 'executed',
submittedBy: 'user',
},
pendingData: {
displayName: 'Send Welcome Email',
Expand Down Expand Up @@ -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',
},
}),
);
});
Expand Down Expand Up @@ -1535,6 +1541,7 @@ describe('TriggerRecordActionStepExecutor', () => {
executionResult: {
success: true,
submissionOutcome: 'pending-approval',
submittedBy: 'user',
submittedValues: { amount: 50 },
aiFilledValues: [{ field: 'amount', value: 50 }],
},
Expand Down
Loading