Workflow

core

Workflow

Definition

A workflow is a defined sequence of tasks, decisions, and actions that transform inputs into desired outputs following a structured execution path. Workflows encode operational knowledge -- the "how" of getting things done -- into repeatable, automatable, and auditable processes. They range from simple linear sequences (validate, transform, store) to complex directed acyclic graphs with conditional branching, parallel execution, error handling, and retry logic.

In software systems, workflows formalize business processes, data processing pipelines, deployment procedures, and quality assurance chains into executable specifications. Rather than relying on implicit knowledge about "what steps to follow," workflows make the process explicit, testable, and measurable.

Overview

The concept of workflow automation has evolved significantly from its origins in business process management (BPM) systems of the 1990s. Modern workflow engines operate at multiple levels of abstraction: infrastructure workflows (CI/CD pipelines, deployment automation), data workflows (ETL/ELT pipelines, stream processing), business workflows (approval chains, order fulfillment), and AI/ML workflows (training pipelines, inference chains, agent orchestration).

In the Prismatic Platform, workflows are a first-class architectural concept implemented through several interconnected systems:

  • AIAD Pipelines: Multi-step agent workflows that compose commands and actions into complex operations
  • Quality Gate Workflows: Automated quality assurance chains that validate code changes before merge
  • Autoevolve Cycles: Self-improvement workflows that scan, analyze, and apply platform enhancements
  • Autoheal Workflows: Automated remediation workflows that detect and fix quality regressions
  • Session Lifecycle Workflows: Structured start/execute/end flows for Claude sessions
  • CI/CD Pipelines: GitLab CI workflows for building, testing, and deploying the platform
  • Data Pipelines: Processing workflows for OSINT data, compliance assessment, and security scanning

Workflow Characteristics

PropertyDescriptionPrismatic Implementation
DeterministicSame inputs produce same outputsPure function steps, immutable state
ObservableExecution progress is visibleTelemetry events, audit logging
RecoverableFailures can be retried or rolled backRetry policies, compensation steps
ComposableWorkflows can include sub-workflowsNested pipeline execution
MeasurableDuration and outcomes are trackedTelemetry metrics, performance monitoring
AuditableEvery step is loggedImmutable audit trail

Technical Details

Workflow Engine Architecture

The Prismatic Platform implements a workflow engine built on OTP principles:

defmodule PrismaticClaude.Workflow.Engine do
  @moduledoc """
  Workflow execution engine built on OTP GenServer.
  Executes multi-step workflows with retry logic,
  timeout handling, and telemetry instrumentation.
  """

  use GenServer

  @type step :: %{
    name: atom(),
    handler: (map() -> {:ok, map()} | {:error, term()}),
    timeout: non_neg_integer(),
    retries: non_neg_integer(),
    retry_delay: non_neg_integer()
  }

  @type workflow :: %{
    id: String.t(),
    name: String.t(),
    steps: [step()],
    on_failure: :halt | :continue | :compensate,
    metadata: map()
  }

  @type execution_state :: %{
    workflow: workflow(),
    current_step: non_neg_integer(),
    context: map(),
    results: [step_result()],
    status: :pending | :running | :completed | :failed | :compensating,
    started_at: DateTime.t(),
    completed_at: DateTime.t() | nil
  }

  @type step_result :: %{
    step_name: atom(),
    status: :success | :failure | :skipped,
    output: term(),
    duration_ms: non_neg_integer(),
    retries_used: non_neg_integer()
  }

  @spec start_link(keyword()) :: GenServer.on_start()
  def start_link(opts) do
    GenServer.start_link(__MODULE__, opts, name: __MODULE__)
  end

  @spec execute(workflow(), map()) :: {:ok, [step_result()]} | {:error, term()}
  def execute(workflow, initial_context \\ %{}) do
    GenServer.call(__MODULE__, {:execute, workflow, initial_context}, :infinity)
  end

  @impl true
  def init(opts) do
    {:ok, %{
      max_concurrent: Keyword.get(opts, :max_concurrent, 5),
      active_workflows: %{},
      completed_count: 0
    }}
  end

  @impl true
  def handle_call({:execute, workflow, context}, _from, state) do
    execution = %{
      workflow: workflow,
      current_step: 0,
      context: context,
      results: [],
      status: :running,
      started_at: DateTime.utc_now(),
      completed_at: nil
    }

    :telemetry.execute(
      [:prismatic, :workflow, :started],
      %{step_count: length(workflow.steps)},
      %{workflow_name: workflow.name, workflow_id: workflow.id}
    )

    result = execute_steps(workflow.steps, context, [])

    :telemetry.execute(
      [:prismatic, :workflow, :completed],
      %{duration_ms: elapsed_ms(execution.started_at)},
      %{workflow_name: workflow.name, status: elem(result, 0)}
    )

    {:reply, result, state}
  end

  @spec execute_steps([step()], map(), [step_result()]) ::
          {:ok, [step_result()]} | {:error, term()}
  defp execute_steps([], _context, results) do
    {:ok, Enum.reverse(results)}
  end

  defp execute_steps([step | remaining], context, results) do
    start_time = System.monotonic_time(:millisecond)
    result = execute_step_with_retries(step, context, 0)
    duration = System.monotonic_time(:millisecond) - start_time

    case result do
      {:ok, updated_context, retries_used} ->
        step_result = %{
          step_name: step.name,
          status: :success,
          output: updated_context,
          duration_ms: duration,
          retries_used: retries_used
        }

        :telemetry.execute(
          [:prismatic, :workflow, :step_completed],
          %{duration_ms: duration, retries: retries_used},
          %{step_name: step.name}
        )

        execute_steps(remaining, updated_context, [step_result | results])

      {:error, reason} ->
        step_result = %{
          step_name: step.name,
          status: :failure,
          output: reason,
          duration_ms: duration,
          retries_used: step.retries
        }

        {:error, {step.name, reason, Enum.reverse([step_result | results])}}
    end
  end

  @spec execute_step_with_retries(step(), map(), non_neg_integer()) ::
          {:ok, map(), non_neg_integer()} | {:error, term()}
  defp execute_step_with_retries(step, context, attempt) do
    task = Task.async(fn -> step.handler.(context) end)

    case Task.yield(task, step.timeout) || Task.shutdown(task) do
      {:ok, {:ok, updated_context}} ->
        {:ok, updated_context, attempt}

      {:ok, {:error, reason}} when attempt < step.retries ->
        Process.sleep(step.retry_delay * (attempt + 1))
        execute_step_with_retries(step, context, attempt + 1)

      {:ok, {:error, reason}} ->
        {:error, reason}

      nil ->
        if attempt < step.retries do
          execute_step_with_retries(step, context, attempt + 1)
        else
          {:error, :timeout}
        end
    end
  end

  @spec elapsed_ms(DateTime.t()) :: non_neg_integer()
  defp elapsed_ms(start) do
    DateTime.diff(DateTime.utc_now(), start, :millisecond)
  end
end

Quality Gate Workflow

The quality gate workflow is one of the most critical workflows in the platform, executing on every code change:

defmodule PrismaticSafety.QualityWorkflow do
  @moduledoc """
  Quality gate workflow that validates code changes
  against platform quality standards. Executes as
  part of pre-commit hooks and CI/CD pipelines.
  """

  alias PrismaticClaude.Workflow.Engine

  @type quality_context :: %{
    changed_files: [String.t()],
    commit_message: String.t(),
    author: String.t(),
    branch: String.t()
  }

  @spec build_quality_workflow() :: Engine.workflow()
  def build_quality_workflow do
    %{
      id: "quality-gates-#{:erlang.unique_integer([:positive])}",
      name: "quality_gates",
      steps: [
        %{
          name: :compilation,
          handler: &check_compilation/1,
          timeout: 120_000,
          retries: 1,
          retry_delay: 5_000
        },
        %{
          name: :credo,
          handler: &check_credo/1,
          timeout: 60_000,
          retries: 0,
          retry_delay: 0
        },
        %{
          name: :dialyzer,
          handler: &check_dialyzer/1,
          timeout: 300_000,
          retries: 0,
          retry_delay: 0
        },
        %{
          name: :tests,
          handler: &run_tests/1,
          timeout: 300_000,
          retries: 1,
          retry_delay: 5_000
        },
        %{
          name: :forbidden_patterns,
          handler: &check_forbidden_patterns/1,
          timeout: 30_000,
          retries: 0,
          retry_delay: 0
        },
        %{
          name: :coverage,
          handler: &check_coverage/1,
          timeout: 60_000,
          retries: 0,
          retry_delay: 0
        }
      ],
      on_failure: :halt,
      metadata: %{enforced_by: "NO MERCY, NO DOUBTS doctrine"}
    }
  end

  @spec execute(quality_context()) :: {:ok, [Engine.step_result()]} | {:error, term()}
  def execute(context) do
    workflow = build_quality_workflow()
    Engine.execute(workflow, context)
  end

  @spec check_compilation(map()) :: {:ok, map()} | {:error, term()}
  defp check_compilation(context) do
    case System.cmd("mix", ["compile", "--warnings-as-errors", "--force"],
           stderr_to_stdout: true) do
      {_output, 0} -> {:ok, Map.put(context, :compilation, :pass)}
      {output, _} -> {:error, {:compilation_failed, output}}
    end
  end

  @spec check_credo(map()) :: {:ok, map()} | {:error, term()}
  defp check_credo(context) do
    case System.cmd("mix", ["credo", "--strict"], stderr_to_stdout: true) do
      {_output, 0} -> {:ok, Map.put(context, :credo, :pass)}
      {output, _} -> {:error, {:credo_violations, output}}
    end
  end

  @spec check_dialyzer(map()) :: {:ok, map()} | {:error, term()}
  defp check_dialyzer(context) do
    case System.cmd("mix", ["dialyzer"], stderr_to_stdout: true) do
      {_output, 0} -> {:ok, Map.put(context, :dialyzer, :pass)}
      {output, _} -> {:error, {:dialyzer_warnings, output}}
    end
  end

  @spec run_tests(map()) :: {:ok, map()} | {:error, term()}
  defp run_tests(context) do
    case System.cmd("mix", ["test", "--cover"], stderr_to_stdout: true) do
      {_output, 0} -> {:ok, Map.put(context, :tests, :pass)}
      {output, _} -> {:error, {:test_failures, output}}
    end
  end

  @spec check_forbidden_patterns(map()) :: {:ok, map()} | {:error, term()}
  defp check_forbidden_patterns(context) do
    case System.cmd("mix", ["quality.forbidden_patterns", "--count-only"],
           stderr_to_stdout: true) do
      {"0\n", 0} -> {:ok, Map.put(context, :forbidden_patterns, :pass)}
      {output, _} -> {:error, {:forbidden_patterns_found, output}}
    end
  end

  @spec check_coverage(map()) :: {:ok, map()} | {:error, term()}
  defp check_coverage(context) do
    # Coverage threshold enforced by mix test --cover configuration
    {:ok, Map.put(context, :coverage, :pass)}
  end
end

Session Lifecycle Workflow

defmodule PrismaticClaude.SessionLifecycle do
  @moduledoc """
  Session lifecycle workflow managing the structured
  start, execute, and end flow for Claude sessions.
  Implements the Universal Autonomous Evolution Protocol.
  """

  use GenServer

  @type session_phase :: :start | :active | :ending | :ended
  @type hook :: %{
    name: atom(),
    handler: (map() -> {:ok, map()} | {:error, term()}),
    priority: non_neg_integer(),
    enabled: boolean()
  }

  @type session_state :: %{
    session_id: String.t(),
    phase: session_phase(),
    started_at: DateTime.t(),
    hooks: [hook()],
    context: map(),
    circuit_breaker: :closed | :open | :half_open,
    failure_count: non_neg_integer()
  }

  @spec start_session(map()) :: {:ok, session_state()} | {:error, term()}
  def start_session(opts \\ %{}) do
    session = %{
      session_id: generate_session_id(),
      phase: :start,
      started_at: DateTime.utc_now(),
      hooks: default_hooks(),
      context: opts,
      circuit_breaker: :closed,
      failure_count: 0
    }

    with {:ok, session} <- execute_phase_hooks(session, :session_start),
         {:ok, session} <- load_session_context(session),
         {:ok, session} <- run_baseline(session) do
      {:ok, %{session | phase: :active}}
    end
  end

  @spec end_session(session_state()) :: {:ok, session_state()} | {:error, term()}
  def end_session(session) do
    session = %{session | phase: :ending}

    with {:ok, session} <- save_session_context(session),
         {:ok, session} <- execute_phase_hooks(session, :session_end),
         {:ok, session} <- run_evolution_cycle(session) do
      {:ok, %{session | phase: :ended}}
    end
  end

  @spec execute_phase_hooks(session_state(), atom()) :: {:ok, session_state()} | {:error, term()}
  defp execute_phase_hooks(session, phase) do
    if session.circuit_breaker == :open do
      {:ok, session}
    else
      hooks =
        session.hooks
        |> Enum.filter(& &1.enabled)
        |> Enum.sort_by(& &1.priority)

      Enum.reduce_while(hooks, {:ok, session}, fn hook, {:ok, sess} ->
        case safe_execute_hook(hook, sess) do
          {:ok, updated} -> {:cont, {:ok, updated}}
          {:error, reason} -> handle_hook_failure(sess, hook, reason)
        end
      end)
    end
  end

  @spec safe_execute_hook(hook(), session_state()) :: {:ok, session_state()} | {:error, term()}
  defp safe_execute_hook(hook, session) do
    task = Task.async(fn -> hook.handler.(session.context) end)

    case Task.yield(task, 30_000) || Task.shutdown(task) do
      {:ok, {:ok, updated_context}} ->
        {:ok, %{session | context: updated_context}}

      {:ok, {:error, reason}} ->
        {:error, reason}

      nil ->
        {:error, :hook_timeout}
    end
  end
end

Parallel Workflow Execution

For workflows with independent steps, the platform supports parallel execution using OTP's Task module:

defmodule PrismaticClaude.Workflow.Parallel do
  @moduledoc """
  Parallel workflow execution for independent steps.
  Uses Task.async_stream for concurrent step execution
  with backpressure control.
  """

  @spec execute_parallel([Engine.step()], map(), keyword()) ::
          {:ok, [Engine.step_result()]} | {:error, term()}
  def execute_parallel(steps, context, opts \\ []) do
    max_concurrency = Keyword.get(opts, :max_concurrency, System.schedulers_online())
    timeout = Keyword.get(opts, :timeout, 60_000)

    results =
      steps
      |> Task.async_stream(
        fn step ->
          start = System.monotonic_time(:millisecond)
          result = step.handler.(context)
          duration = System.monotonic_time(:millisecond) - start
          {step.name, result, duration}
        end,
        max_concurrency: max_concurrency,
        timeout: timeout,
        on_timeout: :kill_task
      )
      |> Enum.map(fn
        {:ok, {name, {:ok, output}, duration}} ->
          %{step_name: name, status: :success, output: output, duration_ms: duration, retries_used: 0}

        {:ok, {name, {:error, reason}, duration}} ->
          %{step_name: name, status: :failure, output: reason, duration_ms: duration, retries_used: 0}

        {:exit, reason} ->
          %{step_name: :unknown, status: :failure, output: reason, duration_ms: 0, retries_used: 0}
      end)

    failures = Enum.filter(results, &(&1.status == :failure))

    if Enum.empty?(failures) do
      {:ok, results}
    else
      {:error, {:parallel_failures, failures, results}}
    end
  end
end

Implementation in Prismatic Platform

Workflow Types in the Platform

WorkflowTriggerStepsFrequency
Quality GatesPre-commit hookCompile, Credo, Dialyzer, Tests, Forbidden PatternsEvery commit
AutoevolveSession end, manualScan, Analyze, Generate, Apply, VerifyEvery session
AutohealQuality regressionDetect, Diagnose, Fix, Verify, ReportOn quality drop
CI/CD PipelineGit pushBuild, Test, Analyze, DeployEvery push
EASM ScanScheduled, on-demandDiscover, Scan, Assess, Rate, ReportDaily/weekly
Compliance AssessmentOn-demandCollect, Assess NIS2, Assess ZKB, Compare, ReportOn request
Session LifecycleSession start/endLoad context, Execute hooks, Save contextEvery session
Agent OrchestrationCommand invocationParse, Route, Execute, Aggregate, ReportPer command

Workflow Observability

All workflows emit telemetry events for monitoring and debugging:

# Workflow-level events
[:prismatic, :workflow, :started]     # %{step_count: N}
[:prismatic, :workflow, :completed]   # %{duration_ms: N}
[:prismatic, :workflow, :failed]      # %{step_name: atom, reason: term}

# Step-level events
[:prismatic, :workflow, :step_started]   # %{step_name: atom}
[:prismatic, :workflow, :step_completed] # %{duration_ms: N, retries: N}
[:prismatic, :workflow, :step_failed]    # %{step_name: atom, reason: term}

Workflow Definition DSL

The platform provides a declarative workflow definition approach:

defmodule PrismaticClaude.Workflow.DSL do
  @moduledoc """
  Declarative workflow definition DSL for building
  workflows from composable step definitions.
  """

  @spec workflow(String.t(), keyword()) :: Engine.workflow()
  def workflow(name, opts \\ []) do
    %{
      id: "#{name}-#{:erlang.unique_integer([:positive])}",
      name: name,
      steps: Keyword.get(opts, :steps, []),
      on_failure: Keyword.get(opts, :on_failure, :halt),
      metadata: Keyword.get(opts, :metadata, %{})
    }
  end

  @spec step(atom(), (map() -> {:ok, map()} | {:error, term()}), keyword()) :: Engine.step()
  def step(name, handler, opts \\ []) do
    %{
      name: name,
      handler: handler,
      timeout: Keyword.get(opts, :timeout, 30_000),
      retries: Keyword.get(opts, :retries, 0),
      retry_delay: Keyword.get(opts, :retry_delay, 1_000)
    }
  end
end

Comparison with Alternatives

ApproachComplexityRecoverabilityObservabilityPrismatic Usage
GenServer WorkflowMediumManual retry/compensateTelemetry eventsQuality gates, session lifecycle
Broadway/GenStageHighBuilt-in backpressureMetrics + dashboardData processing pipelines
ObanMediumBuilt-in retry, schedulingWeb UI, telemetryBackground job workflows
Task.async_streamLowBasic timeoutManual instrumentationParallel scan steps
Shell ScriptsLowExit codes onlystdout/stderrCI/CD helper scripts
GitLab CI YAMLMediumStage-level retryGitLab UIBuild and deploy pipelines

External Workflow Engines Comparison

EngineLanguageModelStrengthWeakness
Prismatic Workflow EngineElixirOTP GenServerFault-tolerant, observablePlatform-specific
Apache AirflowPythonDAG schedulerRich ecosystem, schedulingHeavy, not real-time
TemporalGo/Java/PythonDurable executionExactly-once, long-runningComplex deployment
PrefectPythonTask graphModern Python, cloud-nativePython-only
Step FunctionsAWSState machineServerless, visualVendor lock-in
Argo WorkflowsYAML/GoKubernetes CRDContainer-nativeKubernetes dependency

The Prismatic Platform uses its own lightweight workflow engine rather than an external system because: (1) OTP provides superior fault tolerance through supervision trees, (2) Elixir's concurrency model handles parallel step execution natively, (3) tight integration with platform telemetry and quality systems requires deep coupling, and (4) the workflow definitions are simple enough that an external engine would add unnecessary operational complexity.

Best Practices

  1. Keep steps small and focused: Each workflow step should do one thing well. Small steps are easier to test, retry, and replace. If a step does too much, split it into multiple steps.

  2. Make steps idempotent: Steps that can be safely re-executed produce the same result regardless of how many times they run. Idempotency is essential for retry logic and crash recovery.

  3. Define clear failure modes: Every step should explicitly define what constitutes success and failure. Use {:ok, context} and {:error, reason} tuples consistently. Never let steps fail silently.

  4. Instrument with telemetry: Every workflow and step should emit telemetry events for duration, success/failure, and relevant metadata. This data enables monitoring, alerting, and performance optimization.

  5. Set appropriate timeouts: Every step needs a timeout to prevent hung workflows from blocking resources indefinitely. Set timeouts based on expected execution time with reasonable margin.

  6. Use compensation for side effects: When a workflow fails partway through and earlier steps have produced side effects (database writes, API calls, file creation), implement compensation steps that undo those effects.

  7. Compose rather than nest deeply: Prefer flat workflow compositions over deeply nested sub-workflows. Use the workflow DSL to compose reusable step definitions into different workflow configurations.

  8. Version workflow definitions: As workflows evolve, maintain backward compatibility for in-flight executions. Use versioned workflow definitions so running instances complete with their original definition.

Common Pitfalls

  • Monolithic steps: Combining multiple operations into a single step makes debugging difficult and prevents granular retry. If a step fails, the entire combined operation must be retried.

  • Missing error handling: Steps that raise exceptions instead of returning {:error, reason} crash the workflow engine. Always wrap step logic in try/rescue or use with/case for graceful error handling.

  • Unbounded retries: Retrying failed steps indefinitely can create infinite loops and resource exhaustion. Set maximum retry counts with exponential backoff and circuit breaker patterns.

  • Ignoring partial completion: When a multi-step workflow fails midway, ignoring the completed steps can lead to inconsistent state. Implement either compensation (undo completed steps) or checkpoint-resume (restart from failure point).

  • Synchronous bottlenecks: Running all steps sequentially when some are independent wastes time. Identify independent steps and execute them in parallel using Task.async_stream with backpressure control.

  • Hard-coded configuration: Embedding timeouts, retry counts, and concurrency limits in step definitions reduces flexibility. Use configuration that can be tuned per environment (development, staging, production).

Use Cases

Quality Assurance Pipeline: The pre-commit quality workflow executes six sequential validation steps (compilation, Credo, Dialyzer, tests, forbidden patterns, coverage) on every commit. Any failure blocks the commit, enforcing the platform's zero-defect quality standard.

EASM Scanning Workflow: The EASM scanning workflow discovers external assets, performs vulnerability scanning, assesses compliance against NIS2/ZKB frameworks, calculates security ratings, and generates reports. Steps execute in a defined sequence with parallel sub-steps for scanning multiple assets simultaneously.

Platform Evolution Cycle: The autoevolve workflow scans the codebase for improvement opportunities, analyzes findings against platform quality standards, generates code changes, applies them, verifies through quality gates, and reports results. This workflow runs at every session end as part of the Universal Autonomous Evolution Protocol.

CI/CD Deployment Pipeline: The GitLab CI pipeline workflow builds the application, runs the full test suite, performs static analysis, checks for security vulnerabilities in dependencies, and deploys to staging/production. Each stage depends on the successful completion of previous stages.

Agent Orchestration: When a user invokes a complex AIAD command like /orchestrate, the orchestrator workflow decomposes the request into sub-tasks, routes them to specialist agents, collects results, aggregates findings, and presents a unified response.

See Also

  • Architecture -- Platform architecture overview
  • Technologies -- Technology stack details
  • Apps -- Application ecosystem
  • Agents -- Agent directory executing workflows
  • Commands -- Command catalog composing workflows

Connect & Contribute

Created by Tomas Korcak (korczis) | Open Source under GHL