Skip to content

feat(backend): add conditional action workflows#384

Merged
EDOHWARES merged 2 commits into
EDOHWARES:mainfrom
oderahub:feat/workflows-273
May 1, 2026
Merged

feat(backend): add conditional action workflows#384
EDOHWARES merged 2 commits into
EDOHWARES:mainfrom
oderahub:feat/workflows-273

Conversation

@oderahub
Copy link
Copy Markdown
Contributor

Summary

Implements backend support for conditional, sequential action chaining (workflows) on triggers.

A trigger can now define steps[] to execute multiple actions in order. Step N+1 only runs if step N succeeds (default), or based on per-step runIf rules. Step results are passed forward in a workflow context that later steps can reference via templates.

Design Decisions

  • Schema: steps[] embedded on Trigger — backwards compatible. A trigger with no steps runs through the existing single-action path unchanged.
  • Discriminator: steps.length > 0 selects the workflow path. Joi validation rejects ambiguous combinations (top-level actionUrl plus non-empty steps[]).
  • Conditions: three runIf presets — success (default), failure, always. Full expression conditions deferred to a follow-up.
  • State passing: each step's result is keyed by step id in context.steps, so templates resolve as {{steps.fetchUser.output.userId}} regardless of step order changes.
  • Idempotency: BullMQ retries the whole workflow run on failure, so receivers must be idempotent. Each workflow run carries a runId (UUID or BullMQ job id) injected into webhook payloads so receivers can dedupe across retries. Resumable retries that skip already-completed steps are deferred to a follow-up.

What Changed

Added

  • backend/src/services/workflow.service.js

    • executeWorkflow(trigger, eventPayload, options) walks steps in order
    • Maintains a workflow context: { runId, event, trigger, steps, stepOrder, lastResult }
    • Throws WorkflowExecutionError on failure unless workflowConfig.continueOnError is true
  • backend/src/services/actionExecutor.service.js

    • Shared executeSingleAction(triggerLike, eventPayload, options) used by both single-action and workflow paths
    • Webhook steps automatically receive workflow: { runId, stepId, previousSteps, stepOrder, lastResult } in the signed payload
  • backend/src/utils/templater.js

    • Read-only dot-path resolver: {{steps.<id>.output.<field>}}
    • No eval, no arbitrary expressions; missing paths are left unchanged
  • docs/workflows.md

    • Schema, execution rules, state passing, retry/idempotency notes, continueOnError semantics, and a mocked benchmark script
  • New tests

    • backend/__tests__/workflow.service.test.js — sequential success, default-skip on failure, compensating step via runIf: failure, continueOnError, template resolution, missing-template passthrough
    • backend/__tests__/trigger.workflow.test.js — schema validation, ambiguous-combo rejection on create and update, duplicate step ids, per-step IP whitelist validation
    • backend/__tests__/workflow.processor.test.js — batch-mode workflow dispatch through the processor

Updated

  • backend/src/models/trigger.model.js

    • Adds steps[] (with required regex-validated id) and workflowConfig.continueOnError
    • Top-level actionUrl becomes optional when steps[] is non-empty
  • backend/src/middleware/validation.middleware.js

    • Joi schemas for workflow steps; rejects ambiguous actionUrl + steps[] combos on both create and update
    • Enforces unique step ids and a max of 20 steps
  • backend/src/controllers/trigger.controller.js

    • Webhook IP whitelist validation now iterates over webhook steps (warnings prefixed with step <id>:)
    • updateTrigger $unsets actionUrl when switching from single-action to workflow
  • backend/src/worker/processor.js

    • Refactored: dispatches through executeWorkflow or executeSingleAction
    • Removes ~80 lines of duplicated action-routing logic; batch path now reuses the same dispatch
  • backend/src/worker/poller.js

    • No-queue fallback now uses the shared dispatch path
  • backend/src/worker/queue.js

    • Workflow jobs are named workflow-<triggerId> for easier observability
  • backend/src/routes/docs.routes.js

    • OpenAPI Trigger and TriggerInput schemas updated with workflow fields
  • backend/src/routes/trigger.routes.js

    • validateBody(triggerUpdate) now applied on update (was previously missing)

Behavior

  • Existing single-action triggers run unchanged.
  • Workflow triggers run steps in array order. By default each step runs only if the previous step succeeded.
  • runIf: 'failure' enables compensating steps (e.g. send a Telegram alert if the primary webhook fails).
  • runIf: 'always' runs the step regardless of prior outcome.
  • workflowConfig.continueOnError: true suppresses the terminal throw on a failed run, but later steps with default runIf: 'success' still skip — they must opt in via always or failure to run after a failure.
  • Webhook steps receive workflow context (including runId and stepId) in their signed payload so receivers can dedupe across BullMQ retries.

Tests

node --test backend/__tests__/workflow.service.test.js \
            backend/__tests__/workflow.processor.test.js \
            backend/__tests__/trigger.workflow.test.js \
            backend/__tests__/trigger.ipWhitelist.test.js
  • New tests: 15/15 passing
  • Full backend suite: remaining failures are all pre-existing on main (Mongo connection, missing supertest, jest-style describe in legacy tests) and unrelated to workflow changes
  • git diff --check clean

Out of Scope (Deliberate)

  • Frontend workflow designer UI
  • Resumable retries that skip completed steps on workflow re-execution
  • Parallel/branching steps
  • Full expression conditionals ({{steps.fetchUser.output.status}} == 200)
  • Adding slack to the trigger action enum (existing scope, not workflow-related)

A WorkflowRun persistence model was also considered and deliberately deferred to keep this PR small.

Notes

This PR composes cleanly with prior reliability/security work:


Closes #273

@drips-wave
Copy link
Copy Markdown

drips-wave Bot commented Apr 29, 2026

@oderahub Great news! 🎉 Based on an automated assessment of this PR, the linked Wave issue(s) no longer count against your application limits.

You can now already apply to more issues while waiting for a review of this PR. Keep up the great work! 🚀

Learn more about application limits

@EDOHWARES
Copy link
Copy Markdown
Owner

Nice implementation, lgtm!

@EDOHWARES EDOHWARES merged commit 298e4f3 into EDOHWARES:main May 1, 2026
1 of 4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Backend: Support for Conditional Action Chaining (Workflows)

2 participants