# FlowDrop Upgrade Guide

This document covers breaking changes and upgrade steps for FlowDrop. Since no stable release has been made yet, this serves as a pre-release reference for tracking changes that affect existing installations.

## Table of Contents

- [1.x to 2.0.0](#1x-to-200)
  - [Requirement change: Drupal 11.3](#requirement-change-drupal-113)
  - [Removed: `SessionService::sendMessage()`](#removed-sessionservicesendmessage)
  - [Removed: `InterruptManagerInterface::createInterrupt()` and `InterruptRequiredException::$interrupt`](#removed-interruptmanagerinterfacecreateinterrupt-and-interruptrequiredexceptioninterrupt)
  - [Removed: `AgentExecutorInterface` and `AgentTraceInterface`](#removed-agentexecutorinterface-and-agenttraceinterface)
  - [Removed: legacy `PipelineStorageInterface` orchestrator constructor argument](#removed-legacy-pipelinestorageinterface-orchestrator-constructor-argument)
  - [Removed: StateGraph ApprovalGate subsystem](#removed-stategraph-approvalgate-subsystem)
  - [Removed: legacy `/playground` route](#removed-legacy-playground-route)
  - [`@api` additions that affect third-party code](#api-additions-that-affect-third-party-code)
  - [Behavior change: the StateGraph loop now enforces `timeout`](#behavior-change-the-stategraph-loop-now-enforces-timeout)
  - [Behavior change: a failed node now fails the run, and auto-retry is off by default](#behavior-change-a-failed-node-now-fails-the-run-and-auto-retry-is-off-by-default)
  - [Stored format change: workflow node metadata slimmed to its source of truth](#stored-format-change-workflow-node-metadata-slimmed-to-its-source-of-truth)
  - [For module authors: regenerate shipped node-type and workflow config dependencies](#for-module-authors-regenerate-shipped-node-type-and-workflow-config-dependencies)
  - [Post-Update Steps](#post-update-steps)
- [1.0.0-beta1 to 1.0.0-beta2](#100-beta1-to-100-beta2)
- [1.0.0-alpha5 to 1.0.0-beta1](#100-alpha5-to-100-beta1)
- [1.0.0-alpha4 to 1.0.0-alpha5](#100-alpha4-to-100-alpha5)
- [1.0.0-alpha3 to 1.0.0-alpha4](#100-alpha3-to-100-alpha4)
- [1.0.0-alpha2 to 1.0.0-alpha3](#100-alpha2-to-100-alpha3)
- [1.0.0-alpha1 to 1.0.0-alpha2](#100-alpha1-to-100-alpha2)
- [0.6.x to 1.0.0-alpha1](#06x-to-100-alpha1)
- [Pre-Release to 1.0.0](#pre-release-to-100)
  - [Database Updates](#database-updates)
  - [API Changes](#api-changes)
  - [Configuration Changes](#configuration-changes)
- [Version 0.4.x Changes](#version-04x-changes)
- [Version 0.3.x Changes](#version-03x-changes)

---

## 1.x to 2.0.0

2.0.0 removes the API surface deprecated across the 1.x line. None of these *removals* affect stored workflows, pipelines, or config — they are PHP API and routing changes that affect custom code (orchestrator subclasses, modules calling the removed services, exhaustive `match()` over `@api` enums) and one bookmarked URL. Separately, one **stored format change** slims workflow node metadata to its source of truth (see [Stored format change](#stored-format-change-workflow-node-metadata-slimmed-to-its-source-of-truth)); it migrates via a database update and needs no manual action for UI authors. If you only author workflows through the UI and never wrote PHP against FlowDrop's services, the things that affect you are the [`timeout` behavior change](#behavior-change-the-stategraph-loop-now-enforces-timeout), the [node-failure behavior change](#behavior-change-a-failed-node-now-fails-the-run-and-auto-retry-is-off-by-default), and the [database updates](#post-update-steps) — plus a re-export if you [track workflow config in code](#stored-format-change-workflow-node-metadata-slimmed-to-its-source-of-truth).

Each removed API was deprecated with a `@trigger_error()` shim in 1.x — run your test suite (or browse the site) on the latest 1.x with deprecation reporting on to find call sites before upgrading.

### Requirement change: Drupal 11.3

2.0.0 raises the minimum Drupal core requirement from `^11.2` to `^11.3` (`core_version_requirement` in every `*.info.yml`, and the `drupal/core` constraint in `composer.json`). The bundled `flowdrop_ui_components` build targets the module-loading behavior introduced in 11.3. Update your site to Drupal 11.3 or later before installing FlowDrop 2.0.0; `composer update` will refuse to resolve 2.0.0 on an older core.

### Removed: `SessionService::sendMessage()`

(`flowdrop_session`, deprecated 1.6.0.) Use `SessionTurnService::executeTurn()` for a full chat turn (post + execute, with the concurrent-turn guard and turn events), or `SessionService::postMessage()` to append a message to the room without executing.

```php
// Before
$sessionService->sendMessage($session, $content);

// After — post and execute a turn (TurnOptions is optional)
$sessionTurnService->executeTurn($session->id(), $content);

// After — append to the room only, no execution
$sessionService->postMessage($session, $content);
```

The private scheduling helpers and the then-unused `@queue` / deferred-scheduler constructor dependencies were dropped with it, so `SessionService`'s constructor signature is narrower. If you instantiated it directly (rather than via the container) update the call. See <https://www.drupal.org/node/3593549>.

### Removed: `InterruptManagerInterface::createInterrupt()` and `InterruptRequiredException::$interrupt`

(`flowdrop_interrupt`, deprecated 1.4.0.) Create interrupts by throwing `InterruptRequiredException` with an `InterruptMessageInterface`; for synchronous creation, build an `InterruptCreationData` and call `InterruptFactoryInterface::create()`.

```php
// Before
$interrupt = $interruptManager->createInterrupt($pipeline, $node, $data);
throw (new InterruptRequiredException())->setInterrupt($interrupt);

// After — the normal node-processor path
throw new InterruptRequiredException($interruptMessage);

// After — synchronous creation
$interrupt = $interruptFactory->create(new InterruptCreationData(/* … */));
```

The public `$interrupt` property is gone; read the entity via `getInterrupt()` and the message via `getInterruptMessage()` on the exception. See <https://www.drupal.org/project/flowdrop/issues/3591539>.

### Removed: `AgentExecutorInterface` and `AgentTraceInterface`

(`flowdrop_runtime`, deprecated 1.6.0.) Agent nodes now execute through the standard node-execution path like any other node. `SynchronousOrchestrator`'s optional `$agentExecutor` constructor argument and its `executeAgentNode()` dispatch path are gone.

To run agentic / tool-using logic, implement `\Drupal\flowdrop\Plugin\FlowDropNodeProcessor\ToolsAwareInterface` on your node processor. Tools wired to the node via `tool_availability` edges are injected with `setTools()`, and you return your result from `process()` like any other processor — there is no separate executor, `DependencyGraph`, or `WorkflowDTO` in plugin code.

```php
// After — a tool-using processor
class MyAgentProcessor extends AbstractFlowDropNodeProcessor implements ToolsAwareInterface {
  use ToolsAwareTrait; // provides setTools() + access to the injected tools

  public function process(ParameterBagInterface $params): array {
    // The wired tools are available via the trait; invoke them through the
    // injected invoker and return your result array.
  }
}
```

No shipped code wired an `AgentExecutorInterface`, so existing workflows are unaffected at runtime. See <https://www.drupal.org/node/3593582>.

### Removed: legacy `PipelineStorageInterface` orchestrator constructor argument

(`flowdrop_runtime`, deprecated 1.4.0.) The first constructor argument of `AbstractOrchestratorBase` — and its `SynchronousOrchestrator`, `AsynchronousOrchestrator`, `SynchronousPipelineOrchestrator`, and `flowdrop_stategraph` `StateGraphOrchestrator` subclasses — no longer accepts a `PipelineStorageInterface`. Pass a `PipelineStorageRegistryInterface`. The internal `LegacyPipelineStorageRegistryAdapter` shim is deleted.

```php
// Before — direct instantiation in a test or custom service
$orchestrator = new SynchronousPipelineOrchestrator($pipelineStorage, $jobStorage, /* … */);

// After
$orchestrator = new SynchronousPipelineOrchestrator($pipelineStorageRegistry, $jobStorage, /* … */);
```

All shipped service definitions already inject `@flowdrop_runtime.pipeline_storage_registry`, so container-built orchestrators need no change. This only affects code that constructs an orchestrator directly (most commonly unit tests) or a custom orchestrator subclass that re-declared the union type — narrow it to `PipelineStorageRegistryInterface`. See <https://www.drupal.org/project/flowdrop/issues/1>.

### Removed: StateGraph ApprovalGate subsystem

(`flowdrop_stategraph`, deprecated 1.6.0.) The `approval_gate` node plugin and the entire ApprovalGate surface — `ApprovalGateInterface`, `ApprovalStatus`, `ApprovalGateHandler`, the `flowdrop_stategraph.approval_gate_handler` service, `ApprovalRequiredException`, and the three `ApprovalGate*Event` classes — are removed.

Use the `flowdrop_interrupt` HITL system instead: the `confirmation` node (simple approve/reject) or `form_input` node (approval with notes), backed by interrupt entities with a REST resolution endpoint. Replace `approval_gate` nodes in existing workflows with one of these before upgrading. `StateGraphOrchestrator` still catches `InterruptExceptionInterface`, so the modern interrupt path is unaffected; it no longer advertises the `approval_gates` capability. See <https://www.drupal.org/node/3593796>.

### Removed: legacy `/playground` route

(`flowdrop_playground`.) The back-compat 302 redirect at `/admin/flowdrop/workflows/{workflow}/playground` is gone now that the playground uses per-session URLs. External bookmarks to `/playground` will 404; users land on the workflow's sessions list at `/admin/flowdrop/workflows/{workflow}/session`. Repoint any hardcoded links. `playgroundPage` / `playgroundTitle` / `resolveLegacySession` are dropped from `PlaygroundController`.

### `@api` additions that affect third-party code

These are additive, not removals, but break code that exhaustively handles the `@api` types:

- **`InterruptType` enum gains a `Timer` case.** An exhaustive `match()` over `InterruptType` without a `default` arm will now throw `UnhandledMatchError` — add a `Timer` arm (or a `default`).
- **`FlowDropInterruptInterface` gains `getScheduledAt()` / `setScheduledAt()`.** As with core entity interfaces, only the shipped entity is expected to implement it; a third-party class that implements the interface directly must add the two methods.

#### Newly stabilized `@api` surface

2.0.0 marks the following as `@api`, so you may now build against them with a BC guarantee (see the [BC policy](reference/api/bc-policy.md)). These are purely additive — nothing to change on upgrade:

- `WorkflowLauncherInterface` (launch a run from any context)
- `SessionTurnServiceInterface` (the chat-turn verb)
- `WorkflowDefinitionInterface` (read contract for a saved workflow)
- `WorkflowValidatorInterface` (shared structural validation)
- `ToolPassthroughInterface`, `BranchingNodeProcessorInterface`, `HasSideEffectsInterface` (node-processor capability markers)
- `EdgePortClassifier` (edge classification utility)
- `ResolvedInterruptInterface` (resolved-interrupt DTO contract)

### Behavior change: the StateGraph loop now enforces `timeout`

(`flowdrop_stategraph`.) The orchestration settings form has always defaulted `timeout` to 300 seconds, but the StateGraph loop never enforced it until now. **Workflows that previously ran longer than five minutes will now pause at the budget** (a resumable pause via the system Pause signal, not a failure). If you have long-running StateGraph workflows, raise `timeout` in the workflow's orchestrator settings or set it to `0` (no limit) rather than relying on repeated resumes.

### Behavior change: a failed node now fails the run, and auto-retry is off by default

(`flowdrop_runtime`, `flowdrop_pipeline`.) 2.0.0 introduces opt-in [node failure handling](CHANGELOG.md) (retry, an `error` output port, and error routing). Enabling that meant correcting two behaviors that were quietly wrong before. **No stored data changes and there is no database update** — what changes is how your existing workflows *execute*.

**1. A failed node now fails the run.** Before 2.0.0, when a node processor threw a `\RuntimeException` the runtime swallowed it into an error-status result and the orchestrator marked the job `COMPLETED` anyway. The run continued as though the node had succeeded — downstream nodes received whatever empty or partial output the failed node left behind, and the failure was invisible. As of 2.0.0 a node that fails *fails its job*:

- If the node has an outgoing **error edge** (an edge drawn from its `error` output port), the failure is routed down that edge to your handler and the run continues — see the node-failure-handling entry in the [changelog](CHANGELOG.md).
- If it has **no error edge**, the job fails and the run fails — the same way any uncaught node failure has always ended a run.

**2. Retry is opt-in.** Per-node retry budget now defaults to **0** (previously an implicit **3**). It is authored per node via the **"Max retries"** field in the editor — an injected reserved `maxRetries` config property present on every executable node — and engages only for failures a processor explicitly marks retryable (by throwing `RetryableNodeProcessingException` or an exception implementing `RetryableExceptionInterface`). The budget is read into the job's `max_retries` field at generation (snake_case `max_retries` in raw config is tolerated as a fallback). The job-entity `preSave` logic that silently rewrote a stored `0` into `3` has been removed.

**What this means for an upgraded site.** A node that used to flake intermittently — and was silently retried up to three times, then silently "completed" even when it never succeeded — will now **fail the run on its first error**. If you were (knowingly or not) relying on that masking:

- To restore retries on a node, set its **Max retries** field in the editor (the injected `maxRetries` config property), and make sure the node processor throws a retryable exception for transient errors (retry only re-runs nodes whose processor opts in; a node must be idempotent, because retry re-runs it from the top). Retries are **immediate — there is no delay or backoff between attempts**, so they only help failures that clear on a same-instant re-run; for failures that need a wait first (network timeouts, upstream 5xx, rate limiting), wire the node's **error** port to a Timer/Delay node instead of relying on `max_retries`.
- To handle a failure gracefully instead of failing the run, wire the node's **error** output port to a handler (logging, a fallback path, cleanup).
- To get the old "ignore the failure and keep going" behavior, you must now make it explicit — there is no implicit swallow.

This affects the synchronous, synchronous-pipeline, and StateGraph orchestrators, and the queue-backed asynchronous orchestrator (which executes through the synchronous pipeline orchestrator and so inherits the same behavior).

### Behavior change: port exposure is semantic and enforced by the engine

(`flowdrop_workflow`, `flowdrop_runtime`.) In 2.0.0 a node port is either **exposed** or **not exposed**, and exposure is now meaningful to the engine — not just an editor cosmetic. A not-exposed port is hidden on the canvas, **not wireable, and not runtime-overridable**. Exposure is stored per instance in the injected reserved `ports` config key (see [reserved-names](docs/development/reserved-names.md)); a port the author never touched keeps its metadata default (`exposedByDefault`, which is `true` for ordinary ports). **No stored data changes and there is no database update** — what changes is what the engine accepts and how it resolves values. Three enforcement points, all reading through `\Drupal\flowdrop\Utility\PortExposure`:

- **Save/import/launch validation (`WorkflowValidator`, rule R7).** An edge into a not-exposed input port — or out of a not-exposed output port — is now a blocking validation error (`R7_EDGE_TARGET_NOT_EXPOSED` / `R7_EDGE_SOURCE_NOT_EXPOSED`). The editor already withholds the handle for a not-exposed port, so UI-authored workflows are unaffected; this catches hand-edited or stale imported JSON. The full validator (all rules) also runs at **programmatic/API launch** (`WorkflowLauncher::launch()` — the public launch service, behind `POST /api/flowdrop/workflow/{id}/run`), so a definition that reached storage without save-validation — built through the entity API, or applied via config import — fails fast instead of running to a silently wrong result. An invalid definition throws `WorkflowLaunchValidationException`, which the HTTP endpoint maps to `422` with the same `{code, message, locator}` detail as the save path. (This gate is on the launcher only; the in-editor run/resume surfaces and sub-workflow/tool invocation go through other entry points and rely on the runtime's defensive exposure handling rather than a hard pre-flight.) The reserved `error` output ships **not-exposed**, so wiring an error edge requires exposing that port first (do this in the editor's port panel, or set `config.ports.outputs[].exposed = true`).
- **Parameter resolution (`ParameterResolver`).** A not-exposed connectable param no longer accepts a runtime/LLM value — the author's configured value wins. (Exposed params behave exactly as before.)
- **Tool projection (`ToolParameterScope`).** For a node exposed as an LLM tool, a not-exposed connectable input is treated as pinned: omitted from the model-facing schema and stripped from model args. The pin signal is **exposure**, not value presence — so a param can be pinned while empty, or exposed while carrying a default the model may override. (This path is new in 2.0.0; there is no 1.x behavior to migrate.)

**Action required only if** you author or import workflow JSON outside the editor and have edges touching ports you intend to leave hidden, or you wire the `error` port: expose those ports in the `ports` config (or via the editor) so the edges validate.

### Stored format change: workflow node metadata slimmed to its source of truth

(`flowdrop_workflow`, `flowdrop_node_type`.) A saved workflow node used to carry a `data.metadata` block that duplicated its node type: `executor_plugin`, `name`, `inputs`/`outputs` ports, config defaults, the visual `type`, plus a node-level React Flow `type`. That block was an editor-produced **cache** — authoritative behavior always lives on the node type entity + the processor plugin's parameter schema. 2.0.0 makes that the single source of truth: a node now persists only its node-type anchor (`data.metadata.node_type_id`) plus genuine per-instance input (`data.config`, `data.label`, `position`, and the third-party `data.extensions` bag). The derived fields are stripped on save, re-injected on read for the editor, and re-resolved at execution. The `flowdrop_workflow.node_metadata` config schema is tightened to `{ node_type_id }` to match.

Two further identity cleanups land in the same pass:

- **The node-type anchor was renamed `data.metadata.id` → `data.metadata.node_type_id`.** The old `id` key read ambiguously next to the top-level node `id` (the *instance* id) and is the same concept the execution engine already calls `node_type_id`. Runtime code reads `node_type_id` exclusively — there is **no** fallback to the legacy `id`. The single rename seam is `FlowDropWorkflow::preSave()`, which renames the key on every non-config-sync save; the database update below brings every stored workflow forward. A workflow that re-enters active config *after* the update ran still carrying the legacy `id` (a `drush cim` of old YAML) fails loudly when a reader touches it (an assertion in dev/CI; a misresolved node otherwise) rather than silently misrouting — re-save it, or run the recovery step below.
- **The redundant `data.nodeId` field was removed.** It always duplicated the top-level node `id`; the editor now derives node handle ids from the node's own `id` directly.

In the same pass, transient editor canvas state (`selected`, `dragging`, `deletable`) is dropped on save — it only reflects the editor's pointer state at the instant of save, and the editor re-supplies it on load. The `measured` layout cache (node width/height) is deliberately kept, since it speeds up edge routing, fit-view and auto-layout on the next load.

This is transparent if you author workflows through the UI — there is no manual step for UI authors. The `flowdrop_workflow_post_update_slim_node_identity` database update resaves every stored workflow into the new shape on `drush updb`; workflows not covered by it (e.g. a fresh import) slim on their next save.

**Action required only if:**

- **You read derived node metadata from a stored or exported workflow in custom PHP** — e.g. `$node['data']['metadata']['executor_plugin']`, `['inputs']`, `['outputs']`, or `['name']`. Those keys are no longer persisted. Resolve them from the node type instead: load the `flowdrop_node_type` config entity by `$node['data']['metadata']['node_type_id']` and call `getExecutorPlugin()`, or use the `flowdrop_node_type.metadata_resolver` service (`resolveExecutorPlugin($nodeTypeId)` for the plugin id, `buildForNodeTypeId($nodeTypeId)` for full metadata, or `enrichNodes($nodes)` to re-inject the cache for read-time display). FlowDrop's own readers were migrated; this only affects code you wrote against the stored shape.
- **You track workflow config in code (`drush cex`)** — a YAML exported from 1.x (or hand-authored) still carries the fat metadata, which now produces "missing schema" warnings under the tightened schema on `drush cim`. Re-save each workflow (UI save, or re-export) to slim it; the strip runs on every non-config-sync save. Config sync itself is exempt from the strip (imported config is preserved verbatim), so legacy YAML keeps working until re-saved — it just won't validate cleanly until then. **Recovery:** if config sync re-imports 1.x YAML *after* `drush updb` has run, visit `/admin/flowdrop/workflows/migrate-node-identity` (an `@internal`, admin-only confirm form) to re-save every workflow back into the slim shape in one pass.
- **You consume the `@api` `NodeMetadata` DTO directly** — `NodeMetadata::toArray()` now keys the anchor as `node_type_id` (was `id`); `NodeMetadata::fromArray()` reads `node_type_id` (a legacy `id`-only array asserts). The object property `NodeMetadata::$id` is unchanged. This is independent of the stored shape above: it affects code that calls the DTO directly.

Exported **bundles** are unaffected: a bundle is a portable transport artifact, so export re-inflates the full metadata into the envelope (it must be self-describing for a site that lacks the node types), and the import re-slims it on save.

### For module authors: regenerate shipped node-type and workflow config dependencies

(`flowdrop_node_type`, `flowdrop_workflow`. **Affects any module — contrib or custom — that ships `flowdrop_node_type.*` or `flowdrop_workflow.*` config in `config/install`.**)

A config entity's `dependencies` block is a **snapshot** recomputed only on a non-sync save; config sync (module install) imports it verbatim and never recalculates. Many node-type install files written before per-node-type provider tracking carry an empty `dependencies: {  }` block, which now ships a broken dependency graph. In 2.0.0 this matters because:

- **A workflow now declares a `config` dependency on each node type it references** (`FlowDropWorkflow::calculateDependencies()`), and relies on that node type to carry the `module` dependency on its executor plugin's provider. An empty node-type dependency block breaks the chain — uninstalling the provider module no longer cascades to the workflow, and the node type can be left orphaned.
- **A node type must ship in a module that depends (directly or transitively) on the module providing its executor plugin.** FlowDrop's own `foreach` node type was mis-homed for exactly this reason (its `flowdrop_stategraph:foreach` plugin lives in a module `flowdrop_node_processor` does not depend on) and has been moved to `flowdrop_stategraph`.

**Action required if you ship node types or workflows in `config/install`:**

1. **Regenerate every dependency block** from a fresh recalculation, rather than hand-editing. The reliable way is to install the config on a working site, then re-save and re-export each entity so `calculateDependencies()` runs:

   ```php
   // drush php:script — recompute deps from each install file's own data
   // and write only the dependencies block back, leaving the rest untouched.
   $etm = \Drupal::entityTypeManager();
   foreach (['flowdrop_node_type', 'flowdrop_workflow'] as $etid) {
     foreach ($etm->getStorage($etid)->loadMultiple() as $entity) {
       $entity->calculateDependencies();
       // ...write $entity->getDependencies() into your config/install YAML...
     }
   }
   ```

2. **Verify each declared dependency is satisfiable** within the shipping module's dependency closure: every `module` dep, and the provider of every `config` dep, must be reachable from the module's `.info.yml` dependencies. A node type whose plugin lives in module *X* requires its shipping module to depend on *X*; a workflow referencing node types from module *Y* requires a dependency on *Y*.

3. **Never hand-edit a `dependencies` block** afterward — it will drift from what a save produces. FlowDrop guards its own config with `ConfigDependencyIntegrityTest`; consider an equivalent test in your module.

There is no automatic migration for config shipped by other modules — FlowDrop only regenerates its own. The runtime tolerates a stale block (deps are re-resolved at execution), so this is about a correct **install/uninstall dependency graph**, not runtime behavior.

### Post-Update Steps

```bash
drush updatedb   # database updates listed below
drush cr
```

Database updates in this release:

- **`flowdrop_session_update_10003`** — drops the orphaned legacy `flowdrop_playground_session` / `flowdrop_playground_message` tables. Guarded, so a no-op on sites that never had them.
- **`flowdrop_interrupt` update 10004** — adds the `scheduled_at` base field to the interrupt entity, backing the new Delay / Schedule timer nodes.
- **`flowdrop_workflow_post_update_slim_node_identity`** — resaves every stored workflow so its nodes adopt the slim node-identity format (`data.metadata.node_type_id`, no `data.nodeId`). Batched, idempotent.

After updating, replace any `approval_gate` nodes (see above) and re-export workflow config if you track it in code.

---

## 1.0.0-beta1 to 1.0.0-beta2

### WorkflowExecutionServiceInterface::execute() Mode Parameter (Breaking)

The `$mode` parameter type has changed from `string` to `WorkflowExecutionMode` enum. Code passing string literals or constants will cause a fatal type error.

```php
use Drupal\flowdrop_workflow_executor\Enum\WorkflowExecutionMode;

// Before (broken in beta2)
$service->execute($workflow, $inputData, 'job');
$service->execute($workflow, $inputData, 'job_async');
$service->execute($workflow, $inputData, 'job_fire_forget');

// After
$service->execute($workflow, $inputData, WorkflowExecutionMode::Job);
$service->execute($workflow, $inputData, WorkflowExecutionMode::JobAsync);
$service->execute($workflow, $inputData, WorkflowExecutionMode::JobFireForget);
```

The enum is in `Drupal\flowdrop_workflow_executor\Enum\WorkflowExecutionMode` and carries the same string values (`'job'`, `'job_async'`, `'job_fire_forget'`) as the former constants, so serialized values and config are unaffected — only PHP call sites need updating.

### FlowDropEventTypeInterface::getCategory() Return Type (Breaking)

`FlowDropEventTypeInterface::getCategory()` now declares a typed return of `EventCategory` enum instead of `string`. Any code that type-checks the return value as `string`, passes it where a `string` is expected, or compares it with `===` against a string literal will break.

```php
use Drupal\flowdrop_trigger\Enum\EventCategory;

// Before (broken in beta2)
$category = $eventType->getCategory(); // returned e.g. 'entity'
if ($category === 'entity') { ... }

// After
$category = $eventType->getCategory(); // returns EventCategory enum case
if ($category === EventCategory::Entity) { ... }

// Available cases:
// EventCategory::Entity  ('entity')
// EventCategory::User    ('user')
// EventCategory::Form    ('form')
// EventCategory::Cron    ('cron')
```

If your custom event type plugin previously returned a string from `getCategory()`, update it to return the appropriate `EventCategory` case.

### Post-Update Steps

```bash
drush cr
```

No database updates are required for this release.

---

## 1.0.0-alpha5 to 1.0.0-beta1

No breaking changes. This release focuses on stabilization, documentation, and annotation policy fixes.

### Post-Update Steps

```bash
drush cr
```

No database updates are required for this release.

---

## 1.0.0-alpha4 to 1.0.0-alpha5

### Typed Event System (Breaking)

All FlowDrop event dispatching has migrated from Symfony's `GenericEvent` to 26 purpose-built event classes with `readonly` properties. If you have custom event subscribers listening to FlowDrop events, you must update them.

**Type hint changes:**

```php
// Before
use Symfony\Component\EventDispatcher\GenericEvent;

public function onNodeCompleted(GenericEvent $event): void {
    $nodeId = $event->getArgument('node_id');
    $result = $event->getArgument('result');
}

// After
use Drupal\flowdrop_runtime\Event\NodeCompletedEvent;

public function onNodeCompleted(NodeCompletedEvent $event): void {
    $nodeId = $event->nodeId;
    $result = $event->result;
}
```

**Event name strings are unchanged** — if you subscribe using string event names (e.g., `flowdrop_runtime.node.completed`), those still work. Each event class also provides a `NAME` constant (e.g., `NodeCompletedEvent::NAME`).

**Complete list of new event classes:**

| Module | Event Class | Event Name |
|---|---|---|
| `flowdrop_interrupt` | `InterruptCancelledEvent` | `flowdrop_interrupt.cancelled` |
| `flowdrop_interrupt` | `InterruptCreatedEvent` | `flowdrop_interrupt.created` |
| `flowdrop_interrupt` | `InterruptExpiredEvent` | `flowdrop_interrupt.expired` |
| `flowdrop_interrupt` | `InterruptResolvedEvent` | `flowdrop_interrupt.resolved` |
| `flowdrop_pipeline` | `JobCreatedEvent` | `flowdrop.job.created` |
| `flowdrop_pipeline` | `PipelineJobsGeneratedEvent` | `flowdrop.pipeline.jobs_generated` |
| `flowdrop_runtime` | `BatchExecutionErrorEvent` | `flowdrop_runtime.execution.batch_error` |
| `flowdrop_runtime` | `ExecutionErrorEvent` | `flowdrop_runtime.execution.error` |
| `flowdrop_runtime` | `MonitoringErrorEvent` | `flowdrop_runtime.monitoring.error` |
| `flowdrop_runtime` | `MonitoringNodeExecutionEvent` | `flowdrop_runtime.monitoring.node_execution` |
| `flowdrop_runtime` | `MonitoringStartedEvent` | `flowdrop_runtime.monitoring.started` |
| `flowdrop_runtime` | `MonitoringStoppedEvent` | `flowdrop_runtime.monitoring.stopped` |
| `flowdrop_runtime` | `MonitoringWarningEvent` | `flowdrop_runtime.monitoring.warning` |
| `flowdrop_runtime` | `NodeCompletedEvent` | `flowdrop_runtime.node.completed` |
| `flowdrop_runtime` | `NodePostExecuteEvent` | `flowdrop.node.post_execute` |
| `flowdrop_runtime` | `NodePreExecuteEvent` | `flowdrop.node.pre_execute` |
| `flowdrop_runtime` | `NodeSkippedEvent` | `flowdrop.node.skipped` |
| `flowdrop_runtime` | `PipelineCreatedEvent` | `flowdrop.pipeline.created` |
| `flowdrop_runtime` | `RealTimeBroadcastEvent` | `flowdrop_runtime.real_time_event` |
| `flowdrop_runtime` | `WorkflowCompletedEvent` | `flowdrop.workflow.completed` |
| `flowdrop_runtime` | `WorkflowInterruptedEvent` | `flowdrop.workflow.interrupted` |
| `flowdrop_trigger` | `TriggerPostExecuteEvent` | `flowdrop_trigger.post_execute` |
| `flowdrop_trigger` | `TriggerPreExecuteEvent` | `flowdrop_trigger.pre_execute` |

### Internal Services Made Private (Breaking)

32 services across 6 modules have been marked `public: false`. If your custom code accesses any of these via `\Drupal::service()` or `$container->get()`, switch to dependency injection.

**Affected services:**

- `flowdrop_interrupt`: `logger.channel.flowdrop_interrupt`, `flowdrop_interrupt.persistence`, `flowdrop_interrupt.interrupt_resolved_subscriber`, `flowdrop_interrupt.session_interrupt_resolved_subscriber`
- `flowdrop_orchestration`: `flowdrop_orchestration.helper`
- `flowdrop_runtime`: `flowdrop_runtime.execution_logger`, `flowdrop_runtime.status_tracker`, `flowdrop_runtime.event_broadcaster`, `flowdrop_runtime.workflow_compiler`, `flowdrop_runtime.storage.drupal_pipeline`, `flowdrop_runtime.storage.drupal_job`, `flowdrop_runtime.storage.inmemory_pipeline`, `flowdrop_runtime.storage.inmemory_job`, `flowdrop_runtime.parameter_resolver`, `flowdrop_runtime.data_flow_manager`, `flowdrop_runtime.json_path`, `flowdrop_runtime.workflow_state_manager`, `flowdrop_runtime.entity_snapshot_storage`, `flowdrop_runtime.error_handler`, `flowdrop_runtime.execution_monitor`, `flowdrop_runtime.queue.pipeline_execution`, `flowdrop_runtime.queue.job_execution`
- `flowdrop_session`: `flowdrop_session.execution_service`, `flowdrop_session.deferred_message_processor`, `logger.channel.flowdrop_session`
- `flowdrop_stategraph`: `flowdrop_stategraph.state_manager`, `flowdrop_stategraph.checkpointer_factory`, `flowdrop_stategraph.memory_checkpointer`, `flowdrop_stategraph.entity_checkpointer`, `flowdrop_stategraph.conditional_edge_evaluator`, `flowdrop_stategraph.array_iterator_helper`, `flowdrop_stategraph.orchestrator`

**Migration example:**

```php
// Before (will fail)
$jsonPath = \Drupal::service('flowdrop_runtime.json_path');

// After (use dependency injection)
public function __construct(
    private readonly JsonPathServiceInterface $jsonPath,
) {}
```

### Constructor Signature Changes (Breaking)

Three class constructors were changed. If you subclass any of these, update the `parent::__construct()` call:

| Class | Removed Parameter |
|---|---|
| `StateGraphOrchestrator` | `$configFactory` |
| `FlowDropEndpointConfigService` | Entire constructor removed |

### Exception Hierarchy

All module-level exception classes now extend `FlowDropException` (which itself extends `\RuntimeException`). Existing `catch (\RuntimeException $e)` blocks will continue to work. You can now also catch all FlowDrop errors with `catch (FlowDropException $e)`.

`WorkflowValidationException` has been removed (was unused dead code).

### New Typed Constants

Use the new `@api` constant classes instead of hardcoded strings when building workflow edge data:

```php
use Drupal\flowdrop\Constants\PortDirection;
use Drupal\flowdrop\Constants\EdgeKeys;

$edge[EdgeKeys::SOURCE_HANDLE] = PortDirection::OUTPUT;
```

### Post-Update Steps

```bash
drush cr
```

No database updates are required for this release.

---

## 1.0.0-alpha3 to 1.0.0-alpha4

### API Response Standardization

All API endpoints now return a consistent response envelope:

```json
// Success
{ "success": true, "data": { ... } }

// Error
{ "success": false, "error": "Error message" }

// Paginated
{ "success": true, "data": [...], "pagination": { "total": 100, "limit": 50, "offset": 0, "has_more": true } }
```

**Breaking changes:**
- The `message` field has been removed from all API responses. Use `data` for payload and `error` for errors.
- The `code` field has been removed from error responses. Use HTTP status codes instead.
- The `count` field has been removed from list responses. Use `data.length` client-side or the `pagination` object.
- **WorkflowsController**: Pagination moved from HTTP headers (`X-Total-Count`, `X-Page-Size`, `X-Page-Offset`) to the response body `pagination` object.
- **WorkflowsController**: Hardcoded CORS headers (`Access-Control-Allow-*`) removed. Configure CORS at the server/Drupal level instead.
- **PlaygroundApiController**: The `hasMore` pagination field replaced by the standard `pagination` object.
- **SnapshotApiController**: Pagination format standardized to match the new `pagination` object.

All controllers now use `ApiResponseTrait` from `Drupal\flowdrop\Controller\Api`.

### Entity Changes

- **`FlowDropInterruptInterface`** now extends `EntityChangedInterface` in addition to `ContentEntityInterface` and `EntityOwnerInterface`. Code that type-checks against `FlowDropInterruptInterface` is unaffected. Code that relied on the interface *not* extending `EntityChangedInterface` may need updating.
- **`FlowDropInterrupt`** entity definition converted from `@ContentEntityType` annotation to `#[ContentEntityType]` PHP attribute. No behavioral change.
- **`StateCheckpoint`** class renamed to **`FlowDropStateCheckpoint`**. The entity type ID (`state_checkpoint`) and database table are unchanged. Update `use` statements:
  - `Drupal\flowdrop_stategraph\Entity\StateCheckpoint` → `Drupal\flowdrop_stategraph\Entity\FlowDropStateCheckpoint`
  - `Drupal\flowdrop_stategraph\Entity\StateCheckpointInterface` → `Drupal\flowdrop_stategraph\Entity\FlowDropStateCheckpointInterface`
  - `Drupal\flowdrop_stategraph\Access\StateCheckpointAccessControlHandler` → `Drupal\flowdrop_stategraph\Access\FlowDropStateCheckpointAccessControlHandler`

### Frontend Breaking Changes

**Custom event names** — Update all event listeners:

| Old | New |
|-----|-----|
| `schemaform:change` | `flowdrop:schemaform:change` |
| `schemaform:save` | `flowdrop:schemaform:save` |
| `schemaform:cancel` | `flowdrop:schemaform:cancel` |
| `schemaform:init` | `flowdrop:schemaform:init` |
| `fd-plugin-selected` | `flowdrop:plugin-selector:selected` |

**Drupal behavior names** — Update any code that references behaviors directly:

| Old | New |
|-----|-----|
| `Drupal.behaviors.fdPluginSelector` | `Drupal.behaviors.flowdropPluginSelector` |
| `Drupal.behaviors.fdDepNav` | `Drupal.behaviors.flowdropDependencyNavigator` |

**Data attributes** — Update any CSS or JS that targets these attributes:

| Old | New |
|-----|-----|
| `data-fd-plugin-selector` | `data-flowdrop-plugin-selector` |
| `data-fd-initialized` | `data-flowdrop-initialized` |
| `data-fd-target-input` | `data-flowdrop-target-input` |
| `data-fd-plugin-radio` | `data-flowdrop-plugin-radio` |
| `data-fd-dep-nav` | `data-flowdrop-dependency-navigator` |
| `data-fd-collapse-toggle` | `data-flowdrop-collapse-toggle` |
| `data-fd-copy-json` | `data-flowdrop-copy-json` |
| `data-fd-json` | `data-flowdrop-json` |
| `data-schema-form-initialized` | `data-flowdrop-initialized` |

### Schema Form JS API

The non-functional `mountSchemaForm.update()` method has been removed. If your code called `.update()` on a mounted schema form instance, remove those calls. To re-render with new data, destroy the instance and re-mount.

### Orchestrator ID Detection

Hardcoded orchestrator ID checks (e.g., `if ($executorId === 'synchronous_pipeline')`) have been replaced with capability-based detection via `OrchestratorHelper` methods. If your custom code checks orchestrator IDs directly, migrate to using `OrchestratorHelper::supportsCapability()` or similar helper methods.

### Post-Update Steps

```bash
drush cr
```

No database updates are required for this release.

---

## 1.0.0-alpha2 to 1.0.0-alpha3

### Namespace Changes

Several interfaces and DTOs were relocated to resolve circular dependencies and improve module organization. Update `use` statements:

| Old namespace | New namespace |
|---|---|
| `Drupal\flowdrop_runtime\Service\JsonPath\JsonPathServiceInterface` | `Drupal\flowdrop\Service\JsonPath\JsonPathServiceInterface` |
| `Drupal\flowdrop_orchestration\PipelineExecutorInterface` | `Drupal\flowdrop_runtime\Service\Orchestrator\PipelineExecutorInterface` |
| `Drupal\flowdrop_runtime\DTO\Snapshot\WorkflowSnapshot` | `Drupal\flowdrop_orchestration\DTO\Snapshot\WorkflowSnapshot` |
| `Drupal\flowdrop_runtime\DTO\Snapshot\NodeSnapshot` | `Drupal\flowdrop_orchestration\DTO\Snapshot\NodeSnapshot` |
| `Drupal\flowdrop_interrupt\Exception\InterruptExceptionInterface` | `Drupal\flowdrop_orchestration\Exception\InterruptExceptionInterface` |
| `Drupal\flowdrop_stategraph\DTO\Message` | `Drupal\flowdrop_orchestration\DTO\Snapshot\Message` |
| `Drupal\flowdrop_stategraph\DTO\IteratorState` | `Drupal\flowdrop_orchestration\DTO\Snapshot\IteratorState` |
| `Drupal\flowdrop_runtime\Service\Interrupt\DrupalInterruptPersistence` | `Drupal\flowdrop_interrupt\Service\DrupalInterruptPersistence` |

### Service ID Changes

| Old service ID | New service ID |
|---|---|
| `flowdrop_runtime.interrupt_persistence` | `flowdrop_interrupt.persistence` |

### API Changes

- **`FlowDropWorkflowSnapshot::createFromWorkflowSnapshot()`** now requires a second `EntityStorageInterface $storage` parameter. If you call this method directly, update the call site to pass the storage handler.

### Deprecations

- **`FlowDropPipeline::getReadyJobs()`** is deprecated. Use `JobGenerationService::getReadyJobs()` instead. Will be removed in 1.0.0.

### Internal Refactoring (non-breaking for most users)

These changes are non-breaking for users consuming public APIs, but may affect code that depends on internal implementation details:

- **Procedural hooks converted to PHP 8 attribute-based hook classes** across 7 modules (`flowdrop_workflow`, `flowdrop_job`, `flowdrop_pipeline`, `flowdrop_runtime`, `flowdrop_session`, `flowdrop_interrupt`, `flowdrop_playground`). If you have custom code that patches or extends these `.module` hook functions, update to use the new hook class approach.
- **Magic strings replaced with constant classes**: `PipelineStatus`, `JobStatus`, and `Permissions` constants. If you were using the raw string values directly, consider switching to the constants for forward compatibility.
- **`TriggerManager` split** into `EventFilterService`, `OrchestrationRequestBuilder`, and `TriggerExecutor`. The public `TriggerManagerInterface` remains unchanged.
- **`SessionService` split** by extracting `SessionExecutionService`. The public `SessionServiceInterface` remains unchanged.

### Post-Update Steps

```bash
drush cr
```

No database updates are required for this release.

---

## 1.0.0-alpha1 to 1.0.0-alpha2

### Permission System Overhaul

This release replaces flat permission machine names with granular own/any variants for all content entities. **You must reassign permissions after updating.**

#### Content Entity Permission Changes

| Old permission | New permissions |
|---|---|
| `view flowdrop_job` | `view own flowdrop_job`, `view any flowdrop_job` |
| `edit flowdrop_job` | `edit own flowdrop_job`, `edit any flowdrop_job` |
| `delete flowdrop_job` | `delete own flowdrop_job`, `delete any flowdrop_job` |
| `view flowdrop_pipeline` | `view own flowdrop_pipeline`, `view any flowdrop_pipeline` |
| `edit flowdrop_pipeline` | `edit own flowdrop_pipeline`, `edit any flowdrop_pipeline` |
| `delete flowdrop_pipeline` | `delete own flowdrop_pipeline`, `delete any flowdrop_pipeline` |
| `view flowdrop_workflow_snapshot` | `view own flowdrop_workflow_snapshot`, `view any flowdrop_workflow_snapshot` |
| `edit flowdrop_workflow_snapshot` | `edit own flowdrop_workflow_snapshot`, `edit any flowdrop_workflow_snapshot` |
| `delete flowdrop_workflow_snapshot` | `delete own flowdrop_workflow_snapshot`, `delete any flowdrop_workflow_snapshot` |
| `view state_checkpoint` | `view own state_checkpoint`, `view any state_checkpoint` |
| `edit state_checkpoint` | `edit own state_checkpoint`, `edit any state_checkpoint` |
| `delete state_checkpoint` | `delete own state_checkpoint`, `delete any state_checkpoint` |
| `view flowdrop interrupts` | `view own flowdrop_interrupt`, `view any flowdrop_interrupt` |
| `resolve flowdrop interrupts` | `resolve own flowdrop_interrupt`, `resolve any flowdrop_interrupt` |
| `cancel flowdrop interrupts` | `cancel own flowdrop_interrupt`, `cancel any flowdrop_interrupt` |

#### New Config Entity View Permissions

Non-admin users can now view config entities without full admin access. Assign these as needed:

- `view flowdrop_workflow` — View workflows and their details
- `view flowdrop_node_type` — View node types and their details
- `view flowdrop_node_category` — View node categories

#### Super-Admin Bypass

All access control handlers now check `administer flowdrop` as the first bypass. Users with this permission automatically have full access to all entities across all modules.

### Post-Update Steps

```bash
drush cr
```

After clearing cache, navigate to **People > Permissions** (`/admin/people/permissions`) and reassign the new granular permissions to your roles. No database updates are required.

---

## 0.6.x to 1.0.0-alpha1

### Overview

FlowDrop 1.0 introduces significant architectural changes to prepare for a stable release. Two new modules have been extracted, several interfaces and DTOs have moved to new namespaces, and entity type IDs have been renamed for consistency.

### New Module Dependencies

Two new stable modules were extracted and must be enabled:

```bash
drush en flowdrop_session flowdrop_orchestration
drush cr
```

**`flowdrop_session`** — Extracted from `flowdrop_playground`. Provides session and message entities used by the execution engine. Required by `flowdrop_interrupt`, `flowdrop_playground`, and `flowdrop_workflow_executor`.

**`flowdrop_orchestration`** — Extracted from `flowdrop_runtime`. Provides orchestrator interfaces, DTOs, plugin manager, and helper. Required by `flowdrop_runtime`, `flowdrop_trigger`, and `flowdrop_stategraph`.

### Drupal Version Requirement

Drupal 10 is no longer supported. All modules now require `^11.2`.

### Entity Type Renames

| Old entity type ID | New entity type ID |
|---|---|
| `flowdrop_playground_session` | `flowdrop_session` |
| `flowdrop_playground_message` | `flowdrop_session_message` |

Run `drush updatedb` to migrate existing data. If you have custom code referencing these entity type IDs, update them.

### Service ID Changes

| Old service ID | New service ID |
|---|---|
| `flowdrop_playground.service` | `flowdrop_session.service` |
| `flowdrop_runtime.orchestrator_manager` | `flowdrop_orchestration.plugin_manager` |
| `flowdrop_runtime.orchestrator_helper` | `flowdrop_orchestration.helper` |
| `flowdrop_session.interrupt_resolved_subscriber` | `flowdrop_interrupt.session_interrupt_resolved_subscriber` |

### Namespace Changes

If you have custom code that imports any of the following classes, update the `use` statements:

| Old namespace | New namespace |
|---|---|
| `Drupal\flowdrop_runtime\Exception\InterruptExceptionInterface` | `Drupal\flowdrop_orchestration\Exception\InterruptExceptionInterface` |
| `Drupal\flowdrop_runtime\Service\Interrupt\InterruptPersistenceInterface` | `Drupal\flowdrop_interrupt\Service\InterruptPersistenceInterface` |
| `Drupal\flowdrop_runtime\Service\Interrupt\InterruptState` | `Drupal\flowdrop_interrupt\DTO\InterruptState` |
| `Drupal\flowdrop_runtime\Attribute\Orchestrator` | `Drupal\flowdrop_orchestration\Attribute\Orchestrator` |
| `Drupal\flowdrop_runtime\Service\Orchestrator\OrchestratorInterface` | `Drupal\flowdrop_orchestration\Service\Orchestrator\OrchestratorInterface` |
| `Drupal\flowdrop_runtime\Service\Orchestrator\OrchestratorHelperInterface` | `Drupal\flowdrop_orchestration\Service\Orchestrator\OrchestratorHelperInterface` |
| `Drupal\flowdrop_runtime\Service\Orchestrator\OrchestratorPluginManager` | `Drupal\flowdrop_orchestration\Service\Orchestrator\OrchestratorPluginManager` |
| `Drupal\flowdrop_runtime\Plugin\Orchestrator\OrchestratorPluginInterface` | `Drupal\flowdrop_orchestration\Plugin\Orchestrator\OrchestratorPluginInterface` |
| `Drupal\flowdrop_runtime\Plugin\Orchestrator\OrchestratorPluginBase` | `Drupal\flowdrop_orchestration\Plugin\Orchestrator\OrchestratorPluginBase` |
| `Drupal\flowdrop_runtime\DTO\Orchestrator\OrchestrationRequest` | `Drupal\flowdrop_orchestration\DTO\Orchestrator\OrchestrationRequest` |
| `Drupal\flowdrop_runtime\DTO\Orchestrator\OrchestrationResponse` | `Drupal\flowdrop_orchestration\DTO\Orchestrator\OrchestrationResponse` |
| `Drupal\flowdrop_playground\Entity\FlowDropPlaygroundSession*` | `Drupal\flowdrop_session\Entity\FlowDropSession*` |
| `Drupal\flowdrop_playground\Entity\FlowDropPlaygroundMessage*` | `Drupal\flowdrop_session\Entity\FlowDropSessionMessage*` |
| `Drupal\flowdrop_playground\Service\PlaygroundService` | `Drupal\flowdrop_session\Service\SessionService` |
| `Drupal\flowdrop_session\EventSubscriber\InterruptResolvedSubscriber` | `Drupal\flowdrop_interrupt\EventSubscriber\SessionInterruptResolvedSubscriber` |

### Permission Machine Name Changes

| Old permission | New permission |
|---|---|
| `view any flowdrop_playground_session` | `view any flowdrop_session` |
| `view own flowdrop_playground_session` | `view own flowdrop_session` |
| `create flowdrop_playground_session` | `create flowdrop_session` |
| `delete any flowdrop_playground_session` | `delete any flowdrop_session` |
| `delete own flowdrop_playground_session` | `delete own flowdrop_session` |

### Custom Orchestrator Plugins

If you have custom orchestrator plugins, update:

1. Change the `use` statement for the `#[Orchestrator]` attribute from `Drupal\flowdrop_runtime\Attribute\Orchestrator` to `Drupal\flowdrop_orchestration\Attribute\Orchestrator`.
2. Change base class imports from `Drupal\flowdrop_runtime\Plugin\Orchestrator\OrchestratorPluginBase` to `Drupal\flowdrop_orchestration\Plugin\Orchestrator\OrchestratorPluginBase`.
3. Update any `OrchestrationRequest`/`OrchestrationResponse` imports to the `Drupal\flowdrop_orchestration\DTO\Orchestrator\` namespace.

### API Stability Annotations

All PHP classes now carry one of two annotation levels:

- **`@api`** = Stable public API with full semver BC guarantees
- **`@internal`** = No BC promise, may change in any release (implementation details)

See [docs/reference/api/bc-policy.md](docs/reference/api/bc-policy.md) for the full policy.

---

## Pre-1.0 to 1.0.0

### Database Updates

Run the following after updating to the latest codebase:

```bash
drush updatedb -y
# Or via UI: /update.php
```

#### Required Update Hooks

| Module | Hook | Description |
|--------|------|-------------|
| `flowdrop_runtime` | `10001` | Installs `flowdrop_workflow_snapshot` entity type |
| `flowdrop_pipeline` | `10001` | Adds `snapshot_id` field to `flowdrop_pipeline` entity |

### API Changes

#### 1. ValidationResult is Immutable

**Before (BROKEN):**

```php
$result = ValidationResult::success();
$result->addError("field", "Error message"); // Method does not exist!
```

**After:**

```php
// Option 1: Use static factory methods
return ValidationResult::error("field", "Error message");

// Option 2: Collect errors and create at end
$errors = [];
if ($condition) {
    $errors[] = new ValidationError("field", "Error message");
}
return empty($errors) 
    ? ValidationResult::success() 
    : ValidationResult::failure($errors);

// Option 3: Merge multiple results
$result1 = ValidationResult::error("field1", "Error 1");
$result2 = ValidationResult::error("field2", "Error 2");
return $result1->merge($result2);
```

#### 2. Orchestrator Plugin System

**Before (0.4.2 and earlier):**

Orchestrators were discovered via tagged services in `services.yml`.

**After (0.4.3+):**

Orchestrators use PHP 8 attribute-based plugin discovery:

```php
namespace Drupal\my_module\Plugin\Orchestrator;

use Drupal\flowdrop_runtime\Attribute\Orchestrator;
use Drupal\flowdrop_runtime\Plugin\Orchestrator\OrchestratorPluginBase;

#[Orchestrator(
    id: "my_orchestrator",
    label: new TranslatableMarkup("My Orchestrator"),
    description: new TranslatableMarkup("Description here"),
)]
class MyOrchestratorPlugin extends OrchestratorPluginBase {
    public function execute(OrchestrationRequest $request): OrchestrationResponse {
        // Implementation
    }
}
```

#### 3. NodeProcessorInterface::execute() Removed

**Before (0.3.x):**

```php
public function execute(array $params): array;
```

**After (0.4.0+):**

```php
public function process(ParameterBagInterface $params): array;
```

#### 4. WorkflowSnapshot Integration

The snapshot system is now integrated into all orchestrators. If you have custom orchestrators, ensure they:

1. Generate snapshots after execution
2. Save snapshots via `WorkflowStateManager`
3. Include snapshots in response metadata

```php
// In your orchestrator's execute method:
$snapshot = $this->workflowStateManager->generateSnapshot(
    $workflow,
    $executionId,
    $nodeStates,
    $initialInput
);
$snapshot = $this->workflowStateManager->save($snapshot);

return new OrchestrationResponse(
    success: true,
    data: $result,
    metadata: ["snapshot" => $snapshot]
);
```

#### 5. Pipeline-Snapshot Linking

Pipelines now maintain a reference to their associated workflow snapshot:

```php
// Get snapshot from pipeline
$pipeline = $pipelineStorage->load($id);
$snapshot = $pipeline->getSnapshot();

// Set snapshot on pipeline
$pipeline->setSnapshotId($snapshotEntity->id());
$pipeline->save();
```

### Configuration Changes

#### Trigger Configuration

Trigger categories now default to **disabled**. Enable explicitly in settings:

```yaml
# config/install/flowdrop_trigger.settings.yml
triggers:
  entity:
    enabled: true
  form:
    enabled: true
  user:
    enabled: false
```

---

## Version 0.4.x Changes

### 0.4.3

- **BREAKING**: Orchestrator discovery changed from tagged services to plugin system
- All orchestrators now require `#[Orchestrator]` attribute
- Orchestrator plugins must be in `Plugin/Orchestrator` namespace

### 0.4.1

- Trigger categories default to disabled (security improvement)
- TriggerManager now uses DependencySerializationTrait

### 0.4.0

- **BREAKING**: `NodeProcessorInterface::execute()` removed
- Use `process(ParameterBagInterface $params)` instead
- EntityTrigger split from base Trigger class

---

## Version 0.3.x Changes

### 0.3.0

- `flowdrop_ui` module removed, functionality moved to `flowdrop_ui_components`
- New SDC component architecture for UI elements

---

## Upgrade Checklist

### Upgrading from 1.0.0-beta1 to 1.0.0-beta2

- [ ] Replace string literals passed as `$mode` to `WorkflowExecutionServiceInterface::execute()` with the corresponding `WorkflowExecutionMode` enum case (`::Job`, `::JobAsync`, `::JobFireForget`)
- [ ] Update custom event type plugins: change `getCategory()` return type from `string` to `EventCategory` and return an `EventCategory` enum case
- [ ] Update any call sites that compare `getCategory()` against string literals to compare against `EventCategory` cases
- [ ] Clear cache: `drush cr`

### Upgrading from 1.0.0-alpha5 to 1.0.0-beta1

- [ ] Clear cache: `drush cr`

### Upgrading from 1.0.0-alpha4 to 1.0.0-alpha5

- [ ] Update custom event subscribers: change `GenericEvent` type hints to the specific typed event class (see event table above)
- [ ] Update event property access: replace `$event->getArgument('key')` / `$event->getSubject()` with direct `$event->property` access
- [ ] Replace `\Drupal::service()` calls for any of the 32 services made private with dependency injection
- [ ] Update subclasses of `StateGraphOrchestrator` or `FlowDropEndpointConfigService` for changed constructor signatures
- [ ] Replace `catch (WorkflowValidationException)` if used (class has been removed)
- [ ] Optionally use new `catch (FlowDropException $e)` to catch all FlowDrop errors
- [ ] Optionally use `PortDirection` and `EdgeKeys` constants instead of hardcoded strings
- [ ] Clear cache: `drush cr`

### Upgrading from 1.0.0-alpha3 to 1.0.0-alpha4

- [ ] Update any JS code consuming API responses to use `{ success, data, error }` envelope (removed `message`, `code`, `count` fields)
- [ ] Update pagination logic: use response body `pagination` object instead of `X-Total-Count` / `X-Page-Size` / `X-Page-Offset` headers
- [ ] Configure CORS at the server/Drupal level (hardcoded CORS headers removed from controllers)
- [ ] Update `use` statements for renamed entity classes (see entity changes table above)
- [ ] Update frontend event listeners: `schemaform:*` → `flowdrop:schemaform:*`, `fd-plugin-selected` → `flowdrop:plugin-selector:selected`
- [ ] Update CSS/JS targeting data attributes: `data-fd-*` → `data-flowdrop-*`
- [ ] Update behavior references: `fdPluginSelector` → `flowdropPluginSelector`, `fdDepNav` → `flowdropDependencyNavigator`
- [ ] Remove any calls to `mountSchemaForm.update()` — destroy and re-mount instead
- [ ] Replace hardcoded orchestrator ID checks with `OrchestratorHelper` capability-based detection
- [ ] Clear cache: `drush cr`

### Upgrading from 1.0.0-alpha2 to 1.0.0-alpha3

- [ ] Update `use` statements for moved interfaces and DTOs (see namespace table above)
- [ ] Update service references from `flowdrop_runtime.interrupt_persistence` to `flowdrop_interrupt.persistence`
- [ ] Update calls to `FlowDropWorkflowSnapshot::createFromWorkflowSnapshot()` to pass `$storage` parameter
- [ ] Replace deprecated `FlowDropPipeline::getReadyJobs()` with `JobGenerationService::getReadyJobs()`
- [ ] If patching procedural hooks, migrate to the new PHP 8 hook class approach
- [ ] Clear cache: `drush cr`

### Upgrading from 1.0.0-alpha1 to 1.0.0-alpha2

- [ ] Clear cache: `drush cr`
- [ ] Reassign permissions at **People > Permissions** — all flat permissions replaced with own/any variants
- [ ] Update custom code referencing old flat permission machine names (see permission table above)
- [ ] Assign new config entity view permissions (`view flowdrop_workflow`, `view flowdrop_node_type`, `view flowdrop_node_category`) as needed

### Upgrading from 0.6.x to 1.0.0-alpha1

- [ ] Enable new modules: `drush en flowdrop_session flowdrop_orchestration`
- [ ] Run database updates: `drush updatedb`
- [ ] Clear cache: `drush cr`
- [ ] Update custom code referencing old entity type IDs (`flowdrop_playground_session` / `flowdrop_playground_message`)
- [ ] Update custom code referencing old service IDs (`flowdrop_playground.service`, `flowdrop_runtime.orchestrator_*`)
- [ ] Update `use` statements for moved interfaces and DTOs (see namespace table above)
- [ ] Update permission references if used in custom code or config
- [ ] Update custom orchestrator plugins to use `flowdrop_orchestration` namespace
- [ ] Review [bc-policy.md](docs/reference/api/bc-policy.md) to understand `@api` / `@internal` annotations

### General upgrade checklist

- [ ] All custom orchestrators use plugin system with `#[Orchestrator]` attribute
- [ ] All custom node processors use `process()` instead of `execute()`
- [ ] ValidationResult usage follows immutable pattern
- [ ] Trigger configuration explicitly enables required categories
- [ ] Database updates run successfully (`drush updatedb`)
- [ ] Cache cleared after upgrade (`drush cr`)

## Getting Help

If you encounter issues during upgrade:

1. Check the [CHANGELOG.md](CHANGELOG.md) for detailed release notes
2. Review module-specific READMEs in `modules/*/README.md`
3. Check technical documentation in `docs/development/`

---

*Last updated: March 2026*
