Skip to content

FlowDrop Backward Compatibility Policy

This document defines the backward compatibility (BC) policy for the 2.x line of FlowDrop. FlowDrop follows pragmatic semantic versioning (semver), meaning:

  • Patch releases (2.0.x): Bug fixes only. No BC breaks.
  • Minor releases (2.x.0): New features, deprecations. No BC breaks to stable API.
  • Major releases (x.0.0): May contain BC breaks with migration path documented.

The 1.x line's own promise, and the breaks taken between it and 2.0.0, are recorded in CHANGELOG-1.x.md and the Upgrade Guide. Nothing below applies retroactively to 1.x.

The 2.0.0 alpha and beta releases were explicitly outside this policy — the dev line took breaks freely, which is what the pre-release tags were for. The promise starts at 2.0.0 stable.

API Classification

Every PHP class, interface, trait and enum in FlowDrop falls into one of two categories, indicated by PHPDoc annotations. Every one of them must carry exactly one of @api or @internal.

Scope of that rule: the module's autoloaded PHP — src/ and modules/*/src/. Test classes and the standalone dev scripts under scripts/ are excluded; they are not part of any consumer's surface. An unannotated class is a bug, not an undeclared third state.

At 2.0.0 the rule held with no exceptions: all 679 in-scope classes carried exactly one annotation. It no longer holds. The scope has grown to 754 classes (196 @api, 550 @internal) and 8 of them carry neither — the six Dashboard/DashboardCard classes added by the dashboard extension API, PendingInterruptCard, and FlowDropWorkflow. Read all eight as @internal under the pre-2.0.0 rule below until each is annotated. One of them is a live question rather than an oversight: DashboardCardBase is the base a contrib card extends, and DashboardCardInterface is @api, so it is the same half-internal extension point that PortShapePluginBase was and needs the same answer.

Hook files are not in scope, because the rule is about classes. .module, .install and .post_update.php files declare procedural hook implementations, not classes, so there is nothing in them for @api/@internal to classify. What governs them is Drupal's own contract: a hook implementation is called by core, never by you, and an update hook's stability guarantee is that it runs once and is not renumbered. Treat the functions in them as internal.

An unannotated class read as internal before 2.0.0, and still does. If you depended on something that 2.0.0 annotates @internal, that dependency was never covered — but tell us in the issue queue, because a well-argued case is how a class becomes @api in a 2.x minor.

@api

Classes and interfaces marked @api are the stable public API. They follow strict semver:

  • Method signatures will not change in the 2.x release line.
  • New optional parameters may be added.
  • New methods may be added to classes (but not to interfaces without a deprecation cycle).
  • Removal or renaming requires a major version bump, with the removed symbol marked @deprecated in a prior 2.x minor and the replacement named in the deprecation message.

The stable API includes the FlowDropNodeProcessor plugin system — the classes that external modules use to create custom node types:

  • Plugin discovery: FlowDropNodeProcessor attribute, FlowDropNodeProcessorPluginManager
  • Plugin contracts: FlowDropNodeProcessorInterface, AbstractFlowDropNodeProcessor, NodeExecutorInterface, ConfigEditProviderInterface, ExecutionContextAwareInterface, TriggerNodeProcessorInterface
  • Lifecycle opt-ins (1.4.0+): CancellableNodeProcessorInterface, ResumableNodeProcessorInterface
  • Parameter system: ParameterBag, ParameterBagInterface
  • Data/Output: Data, DataInterface, Output, OutputInterface
  • Validation: ValidationResult, ValidationError, ValidationWarning
  • Execution context: ExecutionContextDTO
  • Node metadata: NodeMetadata, NodeMetadataBuilder, NodePort
  • Config editing: ConfigEdit, DynamicSchemaEndpoint, ExternalEditLink, ConfigEditApiEndpoints

The Orchestrator plugin system is part of the stable API — the classes that external modules use to create custom workflow execution strategies:

  • Plugin discovery: Orchestrator attribute
  • Plugin contracts: OrchestratorPluginInterface, OrchestratorPluginBase, OrchestratorInterface
  • Interrupt contracts: InterruptExceptionInterface, FlowDropInterruptException

The Trigger system and Workflow Executor APIs are stable:

  • Trigger contracts: TriggerManagerInterface, FlowDropTriggerConfigInterface, FlowDropEventTypeInterface
  • Workflow executor contracts: WorkflowExecutionServiceInterface, WorkflowExecutionMode, WorkflowExecutionStatus

The StateGraph, Interrupt, and supporting APIs are also stable:

  • StateGraph contracts: StateManagerInterface, CheckpointerInterface, StateAwareProcessorInterface, ReducerInterface
  • StateGraph data: GraphState, StateUpdate, Checkpoint
  • StateGraph exceptions: MaxIterationsException, StateValidationException, CheckpointException
  • Interrupt contracts: InterruptManagerInterface
  • Interrupt data: InterruptState, InterruptType
  • Interrupt message bus (1.4.0+): InterruptMessageInterface, AbstractInterruptMessage, HitlInterruptMessage, LinkedInterruptMessage, InwardSignalMessage. The concrete shapes are the DTOs a node processor throws via InterruptRequiredException; the bus and handler registry (InterruptMessageBusInterface, InterruptFactoryInterface, InterruptCreationData) are part of the stable surface too.

The Pipeline runtime contract is stable (1.4.0+):

  • Runtime contract: FlowDropPipelineRuntimeInterface — the narrow runtime view of a pipeline used by the storage layer, orchestrators, events, and queue workers. The full FlowDropPipelineInterface (entity-shaped) is @internal; type-hint the runtime interface in code that does not need entity-level access.

The FlowDropSchema form builder is stable (1.4.0+):

  • Form builder contract: FlowDropSchemaFormBuilderInterface — renders Drupal form arrays from FlowDropSchema fragments. FlowDropSchema is a documented subset of JSON Schema; see FlowDropSchema for the supported keyword set. The concrete FlowDropSchemaFormBuilder implementation is @internal.

The port shape plugin system is stable (2.4.0+) — the classes an external module uses to declare a named lane, so a port can say it carries an Order rather than merely an object:

  • Plugin discovery: PortShape attribute. The plugin id IS the lane id and is deliberately not namespaced by provider module, because a lane id is a vocabulary word written by hand into port schemas.
  • Plugin contracts: PortShapePluginInterface, PortShapePluginBaseextend the base. The interface names the contract, but definition() returns PortShapeDefinition, which is @internal and stays that way, so a bare implementation would have to build an internal type. The base implements definition() final and asks schema(), which is the method a computed shape overrides. Its values() accessor is part of the promise too: a computed shape reads its own declared attribute values through it.
  • Config-entity contract: PortShapeInterface — the read contract for a flowdrop_port_shape config entity, the site-owned half of the same vocabulary. Its RESERVED_IDS constant is part of it. The entity CLASS, its form and its list builder are @internal, as entity classes always are here.

What is not in it, and deliberately: PortShapeDefinition (the registry's own value object), PortShapeRegistry, PortShapePluginManager, and PortConfiguration with its interface. Note the asymmetry with the node-processor system, where the plugin manager IS @api — declaring a shape needs the attribute and the base, not the manager, and the served payload is how anything else reads the vocabulary.

The shapes FlowDrop ships (MessagesShape, ErrorShape) are @internal, on the same footing as the shipped node processors: the classes are ours to restructure, while the lane ids messages and error and the schemas they declare are depended on and are covered by the served payload and the Configuration Schema rules.

Nothing validates a value against a shape's schema. Enforcement is a property of a run, not of a type, and it is not built; a shape gates which ports may be wired together and tells a reader what travels, and that is all it promises.

The memory backend plugin system is stable — the classes an external module uses to store FlowDrop memory somewhere else:

  • Plugin contracts: MemoryBackendInterface, AbstractMemoryBackend (the base a custom backend extends for its storage-key helpers)
  • Failure signal: MemoryBackendException — what MemoryManager documents itself as throwing, and what a backend raises when an operation fails

The following surface was stabilized in 2.0.0:

  • Workflow launch: WorkflowLauncherInterface — the supported way to start a workflow run from any context (HTTP, drush, triggers, other modules); replaces starting a run by POSTing a chat message. Its call and failure surface is stable with it: LaunchOptions, LaunchResult, WorkflowLaunchException and its subclasses InvalidLaunchInputException (bad payload) and WorkflowLaunchValidationException (invalid stored definition, carrying the ValidationResult).
  • Session turns: SessionTurnServiceInterface — the single "chat turn" verb (post a message and execute the session's workflow). Access is enforced by the calling route layer, not the service. Stable with it: TurnOptions, TurnResult, and the three exceptions that distinguish a refused turn from a broken session — ConcurrentTurnException, InvalidTurnInputException, MissingWorkflowException.
  • Workflow diagnostics ("the doctor"): WorkflowDoctorInterface, WorkflowDiagnosticInterface (implemented by tagged flowdrop_workflow.diagnostic services), and WorkflowMutatorInterface. The values that cross those contracts are stable too: DiagnosticContext, Finding, RemedyOption, Severity, DoctorReport, RemedyPlan, RemedySelection, and the mutation primitives (RemoveNode, RemoveEdge, RemoveExposureEntry, SetNodeConfig, RemoveNodeConfigKey, ReplaceNodeType). The mutation vocabulary is closed: the mutator dispatches on those concrete classes, so a custom diagnostic composes fixes from them rather than implementing WorkflowMutationInterface itself.
  • Workflow definition: WorkflowDefinitionInterface — the read contract for a saved workflow (nodes, edges, ports, metadata) shared by the form, API, and bundle import.
  • Workflow validation: WorkflowValidatorInterface — the shared structural-validation choke point used by the entity form, the REST API, and bundle import.
  • Node-processor capability markers: ToolPassthroughInterface, BranchingNodeProcessorInterface, HasSideEffectsInterface — opt-in interfaces a custom processor implements to declare tool-passthrough, branch-gating, and side-effect semantics.
  • Edge classification: EdgePortClassifier — the utility orchestrators and the compiler share to classify edges (data / tool-availability / branch) consistently.
  • Resolved interrupt: ResolvedInterruptInterface — the DTO contract for a resolved interrupt handed back to the orchestrator.

The bundle/marketplace transport subsystem (signing, verification, trusted publishers, capability manifest, node-type generation, bundle import/export) is @internal in 2.0.0: the PHP classes may change without notice. Its persisted surface — the flowdrop_trusted_publisher config entity type and its config schema — is, like all config/entity surface, covered by the Configuration Schema and Entity Type rules below.

@internal

Classes marked @internal are implementation details with no BC promise:

  • May change, move, or be removed in any release (including patches) without notice.
  • Should not be extended, instantiated, or type-hinted against by external code.
  • Typically also marked final to prevent extension.

This includes all classes outside the @api surface: entity classes and interfaces, controllers, forms, list builders, hook implementations, concrete service implementations, storage classes, DTOs, exception classes, orchestrator plugins, and queue workers.

Two consequences worth stating plainly, because both look like exceptions and are not:

  • The concrete implementation behind an @api interface is @internal. WorkflowDoctor, SessionTurnService, FlowDropSchemaFormBuilder, ScopedToolInvoker and the shipped memory backends may be replaced, renamed or restructured at any time. Type-hint the interface; do not extend or instantiate the class.
  • The node processors FlowDrop ships are @internal. The plugin system is @api and your own processors are yours to keep; our built-ins (Reason, ToolBox, ToolInvoke, the memory processors, and the rest) are not a base to extend or a signature to rely on. Their plugin ids and their declared ports are the part authors depend on, and those are covered by the Entity Type and Configuration Schema rules below, because that is what saved workflows reference.

The orchestrator surface is annotated inconsistently

This one is an exception, and it is a defect in this document's own classification rather than a rule.

OrchestratorInterface is @api, but its only method — orchestrate(OrchestrationRequest): OrchestrationResponse — is typed entirely in @internal DTOs, as are WorkflowSnapshot and NodeSnapshot, which a caller reaches through the request. So an orchestrator implemented outside this repo must read @internal classes in order to implement the @api interface it is asked to implement. Every class is annotated; two of them disagree about where the boundary falls. Either those DTOs should be @api or the interface overclaims.

What we promise until it is resolved: treat the DTOs reached from OrchestrationRequestOrchestrationRequest, OrchestrationResponse, WorkflowSnapshot, NodeSnapshot and their accessors — as covered by the @api promise in practice. We will not change or remove them in a 2.x release on the grounds that they are tagged @internal.

If you need this resolved rather than worked around, open an issue in the FlowDrop issue queue on Drupal.org and contact the maintainers there. Which way it is resolved depends on who turns out to be implementing orchestrators outside this repo and what they read — and we cannot see that from here, so an issue describing your implementation is the evidence that decides it. A well-argued case is also how a class becomes @api in a 2.x minor, without waiting for 3.0.0.

Resolution is deferred to 3.0.0. 2.0.0 froze the question rather than answering it, which is the honest description of the trade: nothing was removed, so no existing orchestrator broke, but the boundary is not yet a lookup.

HTTP API Stability

FlowDrop's HTTP endpoints under /api/* (defined by *ApiController classes — pipeline, playground, interrupt, job, trigger, snapshot, chat) are @internal. They exist to serve the bundled @flowdrop/flowdrop JavaScript library (the FlowDrop Editor and Playground components) and are not a public REST contract.

  • Endpoint paths, query parameters, request bodies, and response shapes may change between any two FlowDrop releases — including minor releases — without a deprecation cycle.
  • The HTTP layer ships in lock-step with the bundled JS library; consumers should use the matching JS library version rather than calling endpoints directly.
  • The OpenAPI document at docs/development/api/openapi.yaml describes the current shape for reference only and is not a stability promise.

Why these endpoints can't be a stable contract

The HTTP layer is the interop boundary between the Drupal backend and the FlowDrop Editor / Playground JS components. Both halves are versioned together and ship from this repository as a single unit, and the wire format is dictated by what the editor of a given version needs to render and operate on. That coupling is structural, not a temporary state we plan to grow out of:

  1. Editor features extend the wire shape. Every editor capability that surfaces backend state — new node-type metadata, port typing, visual hints, execution telemetry, interrupt shapes — adds or restructures fields on the response. The 1.4 series added typed I/O ports, schema snapshots, lifecycle events, pipeline bundles, linked interrupts, and a HITL inbox; each of those reshaped at least one endpoint to carry the new state. Holding the wire format frozen would either freeze editor evolution or force every new field through a parallel side-channel, both of which we'd reject in review.
  2. The Drupal module and the editor are released together. A given Drupal site runs the JS library bundled in the matching FlowDrop module version. Within a single deployment there is exactly one client and exactly one server, and they're version-matched by construction. The classic motivation for a stable REST contract — independently deployed clients you cannot coordinate with — does not apply.
  3. Versioning the URL would not buy stability, only duplication. A /api/v1 / /api/v2 scheme would require us to maintain both shapes in the same Drupal release, which means twice the controller code, twice the surface area for security review, and a forced freeze on whichever version is declared "stable." Given (2), the result is cost without a corresponding consumer.
  4. This matches Drupal core's posture. The internal AJAX, layout-builder, big-pipe, and CKEditor endpoints that core uses to power its admin UI are not part of core's BC promise either, for the same structural reason: they're a private interop layer between core PHP and core JS, not a public REST API.

If you need a stable programmatic interface, type-hint against the @api PHP classes listed above (the FlowDropNodeProcessor, Orchestrator, Trigger, Workflow Executor, and StateGraph contracts) rather than the HTTP endpoints. For an integration that genuinely needs to be an HTTP client of FlowDrop independent of the bundled editor, open an issue describing the use case so we can decide whether to expose a separate, deliberately-stable surface for it.

Configuration Schema

Configuration schemas (.schema.yml) follow these rules in 2.x:

  • Existing config keys will not be removed or renamed.
  • New keys may be added with sensible defaults.
  • Schema changes are additive-only.
  • Config export/import compatibility is maintained within the 2.x line.

This covers the persisted shape a site owns: node type and workflow config entities, module settings, and the node plugin ids those entities reference. A shipped node processor being @internal does not license removing its plugin id out from under a saved workflow — that is a config break, and it follows these rules and gets an update hook.

Node-type and workflow config dependencies (for module authors)

A flowdrop_node_type or flowdrop_workflow config entity's dependencies block is a derived snapshot — recomputed by calculateDependencies() only on a non-sync save, and imported verbatim by config sync. Modules that ship these entities in config/install own keeping that snapshot correct:

  • A node type must declare the module dependency on its executor plugin's provider, and must ship in a module whose dependency closure includes that provider. (FlowDrop derives this automatically on save; an empty dependencies: { } block shipped in config/install is a bug.)
  • A workflow declares a config dependency on each node type it references (2.0.0+); the shipping module must depend on every module providing a referenced node type.
  • Regenerate dependency blocks from a fresh save/recalculation — never hand-edit them. See the Upgrade Guide for the procedure and rationale.

FlowDrop regenerates only its own shipped config; contrib and custom modules must regenerate theirs.

Entity Type Stability

Entity type IDs and their base field definitions are stable in 2.x:

  • Entity type machine names will not change.
  • Base fields will not be removed (new fields may be added via update hooks).
  • Storage schema changes will always include update hooks.

Plugin System Stability

These extension points are stable in 2.x:

FlowDropNodeProcessor — for creating custom node types:

  • The FlowDropNodeProcessor attribute and FlowDropNodeProcessorInterface will remain backward compatible.
  • Plugin discovery mechanisms will not change.

Orchestrator — for creating custom workflow execution strategies:

  • The Orchestrator attribute, OrchestratorPluginInterface, and OrchestratorPluginBase will remain backward compatible.
  • The interrupt contracts (InterruptExceptionInterface, FlowDropInterruptException) will remain backward compatible.
  • Concrete orchestrator implementations (e.g., SynchronousOrchestratorPlugin) are @internal and not covered by BC guarantees.

MemoryBackend — for storing FlowDrop memory somewhere else:

  • MemoryBackendInterface and AbstractMemoryBackend will remain backward compatible.
  • The shipped backends (entity, cached, static) are @internal.

Tagged workflow diagnostics — for teaching the doctor to detect and repair a new class of problem:

  • WorkflowDiagnosticInterface and the values it exchanges (see the 2.0.0 list above) will remain backward compatible.
  • Register with the flowdrop_workflow.diagnostic service tag; the tag name is part of the contract.

New plugin types may be added in minor releases but are not covered by BC guarantees until marked @api.

Deprecations

A symbol on its way out is marked @deprecated in flowdrop:2.x.0 and is removed from flowdrop:3.0.0. Use \Some\Replacement instead. — the Drupal core format — in the minor that supersedes it, and removed no earlier than the next major. Deprecated code keeps working, and keeps its tests, until it is removed.

Where a runtime deprecation notice would fire on a hot path (per node execution, per poll) we document the deprecation instead of triggering it, rather than flooding a site's log.

Drupal and PHP Version Support

FlowDrop 2.x requires Drupal ^11.3 and PHP 8.3. Drupal 10 is not supported. (FlowDrop 1.x required ^11.2; the 2.0.0 release raises the floor to 11.3.)

Raising either floor within the 2.x line is a minor-release change, not a major one — it follows Drupal core's own support windows. It will be called out in the release notes, and 2.x will not raise a floor above what the current Drupal core release requires.

Reporting BC Breaks

If you believe a release introduced an unintended BC break to stable API, please report it in the FlowDrop issue queue on Drupal.org. Unintended BC breaks to stable API will be treated as bugs and fixed in a patch release.