Execution Safety
Every safety control the brief asked for, what it actually does, and its honest limits.
Dry-run mode
Workflow.isDryRun (default true for every new workflow — the wizard starts every workflow in this mode). When set, WorkflowEngine never constructs the real ActionExecutor for that workflow's actions — it calls describeActionForDryRun() (src/lib/automation/safety/ dry-run.ts, pure) and logs the description to workflow_logs instead, marking the run skipped_dry_run. Directly tested (tests/automation-dry-run.test.ts, tests/automation-workflow-engine.test.ts's "never touches the real ActionExecutor" case) — a dry-run workflow genuinely cannot execute a real action, not just "usually doesn't."
Simulation
There's no separate "simulate" mode beyond dry-run + Manual Trigger: set a workflow to dry-run, then use "Run Now" — it evaluates the real trigger/condition logic against real current data and logs exactly what it would have done, without touching anything. This is the same WorkflowEngine.evaluateAndExecuteForUser() path a live workflow would take, just with the action-execution step swapped for a description.
Confirmation mode
Workflow.confirmationMode. When set, a matched (trigger + conditions both true) workflow does not execute automatically — it's recorded pending_confirmation and the user is notified. Actions only run once the user explicitly clicks "Confirm & Run" (confirmRunAction -> WorkflowEngine.confirmRun()), which reuses the exact same action- execution path a normal trigger match would use. A manual "Run Now" bypasses confirmation mode by design (the user is already explicitly asking for it to run).
Conflict detection
detectConflicts() (src/lib/automation/safety/conflict-detector.ts, pure, unit tested) flags when a new/edited workflow and an existing active one share a trigger type and contain opposing actions (pause_copying↔resume_copying, stop_copying↔resume_copying, reduce_risk↔increase_risk, enable_strategy↔disable_strategy). Advisory, not blocking — a user may have a legitimate reason to pause on one signal and resume on another; this surfaces the tension rather than silently ignoring it.
Infinite-loop prevention
Two independent, layered guards (src/lib/automation/safety/ loop-guard.ts, both pure, unit tested):
1. Per-workflow cooldown — Workflow.cooldownSeconds. A workflow that just fired won't fire again until the cooldown elapses, even if its trigger condition is still (or newly) true on the next evaluation. 2. Per-batch action cap — RunCapGuard, default 10 actions per evaluateAndExecuteForUser() call. A pathological configuration (many workflows whose actions could satisfy each other's conditions) can't cascade unboundedly within one evaluation pass — anything beyond the cap is logged blocked_safety and skipped, not silently dropped. A fresh guard is constructed per pass; it's not a cross-request rate limiter (that's checkAIRateLimit's job for the AI-calling pieces specifically).
Execution logs
Every evaluation that reaches a matched trigger+conditions creates one workflow_runs row (pending → running → a terminal status: succeeded/failed/skipped_dry_run/blocked_safety/ pending_confirmation) and one workflow_logs row per step (trigger_evaluated, condition_evaluated, action_executed, safety_block, error) — the full trail is visible on /dashboard/workflows/history, expandable per run.
Rollback
RollbackService (src/lib/automation/rollback-service.ts) reverses the subset of a run's actions that have a real, meaningful inverse — recorded as rollback_data on the workflow_runs row by ActionExecutor itself at execution time:
| Action | Reversible? | How |
|---|---|---|
reduce_risk / increase_risk | Yes | Restores the exact pre-action risk_value snapshot |
pause_copying / resume_copying / stop_copying / enable_strategy / disable_strategy | Yes | Calls the inverse CopyEngine method on the same relationship(s) |
send_notification / send_email / execute_webhook | No | A sent message can't be unsent |
close_positions | No | A closed trade can't be reopened at the same price |
generate_ai_report / trigger_ai_analysis / create_journal_entry | No | Generated content, not a state change |
Rolling back a run with no reversible actions returns a clear {success: false, detail: '...'} rather than pretending to succeed. Every rollback is audit-logged (automation_workflow_rollback).
Audit trail
Every workflow create/update/delete/status-change/manual-run/confirm/ rollback, and every admin template deletion, goes through the existing logAuditEvent() (src/lib/security/audit-log.ts) — visible on the existing Audit Center, same as every other audited action in this codebase.
What execution safety does NOT cover
- No protection against a workflow with a *correct but undesirable*
action (e.g. a user deliberately configuring reduce_risk to 0%) — safety controls guard against runaway/unintended cascades, not bad user judgment.
- No cross-workflow transactional guarantee — two workflows executing
in the same pass each commit their own action independently; there's no rollback-everything-if-one-fails semantics across workflows.