feat: extract headless workflow engine

This commit is contained in:
2026-07-31 16:58:51 +02:00
commit 9bccbd68da
54 changed files with 16275 additions and 0 deletions
@@ -0,0 +1 @@
"""Workflow database migrations."""
@@ -0,0 +1,124 @@
"""v0.1.14 module Workflow standards and contribution provenance
Revision ID: 0b4e7c9a2d6f
Revises: f1b7d3e5a9c2
Create Date: 2026-07-31 00:00:00.000000
"""
from __future__ import annotations
from alembic import op
import sqlalchemy as sa
revision = "0b4e7c9a2d6f"
down_revision = "f1b7d3e5a9c2"
branch_labels = None
depends_on = None
def upgrade() -> None:
with op.batch_alter_table("workflow_definitions") as batch:
batch.add_column(
sa.Column("standard_origin_module_id", sa.String(120), nullable=True)
)
batch.add_column(
sa.Column("standard_definition_key", sa.String(120), nullable=True)
)
batch.add_column(
sa.Column(
"standard_origin_module_version",
sa.String(40),
nullable=True,
)
)
batch.add_column(
sa.Column(
"standard_contribution_schema_version",
sa.String(40),
nullable=True,
)
)
batch.add_column(
sa.Column("standard_contribution_hash", sa.String(64), nullable=True)
)
batch.add_column(
sa.Column(
"standard_reconciled_at",
sa.DateTime(timezone=True),
nullable=True,
)
)
batch.create_index(
"ix_workflow_definitions_standard_origin_module_id",
["standard_origin_module_id"],
)
batch.create_index(
"ix_workflow_definitions_standard_definition_key",
["standard_definition_key"],
)
batch.create_index(
"ix_workflow_definitions_standard_contribution_hash",
["standard_contribution_hash"],
)
batch.create_index(
"uq_workflow_standard_origin_scope",
[
"scope_key",
"standard_origin_module_id",
"standard_definition_key",
],
unique=True,
)
with op.batch_alter_table("workflow_definition_revisions") as batch:
batch.add_column(
sa.Column(
"contribution_origin_module_version",
sa.String(40),
nullable=True,
)
)
batch.add_column(
sa.Column(
"contribution_schema_version",
sa.String(40),
nullable=True,
)
)
batch.add_column(
sa.Column("contribution_hash", sa.String(64), nullable=True)
)
batch.add_column(
sa.Column("contribution_metadata", sa.JSON(), nullable=True)
)
batch.create_index(
"ix_workflow_definition_revisions_contribution_hash",
["contribution_hash"],
)
def downgrade() -> None:
with op.batch_alter_table("workflow_definition_revisions") as batch:
batch.drop_index(
"ix_workflow_definition_revisions_contribution_hash"
)
batch.drop_column("contribution_metadata")
batch.drop_column("contribution_hash")
batch.drop_column("contribution_schema_version")
batch.drop_column("contribution_origin_module_version")
with op.batch_alter_table("workflow_definitions") as batch:
batch.drop_index("uq_workflow_standard_origin_scope")
batch.drop_index(
"ix_workflow_definitions_standard_contribution_hash"
)
batch.drop_index("ix_workflow_definitions_standard_definition_key")
batch.drop_index(
"ix_workflow_definitions_standard_origin_module_id"
)
batch.drop_column("standard_reconciled_at")
batch.drop_column("standard_contribution_hash")
batch.drop_column("standard_contribution_schema_version")
batch.drop_column("standard_origin_module_version")
batch.drop_column("standard_definition_key")
batch.drop_column("standard_origin_module_id")
@@ -0,0 +1 @@
"""Workflow Alembic revisions."""
@@ -0,0 +1,201 @@
"""v0.1.14 Workflow definitions
Revision ID: a7c4e2f9b1d3
Revises: None
Create Date: 2026-07-28 00:00:00.000000
"""
from __future__ import annotations
from alembic import op
import sqlalchemy as sa
revision = "a7c4e2f9b1d3"
down_revision = None
branch_labels = None
depends_on = None
def upgrade() -> None:
op.create_table(
"workflow_definitions",
sa.Column("id", sa.String(length=36), nullable=False),
sa.Column("tenant_id", sa.String(length=36), nullable=False),
sa.Column("definition_key", sa.String(length=120), nullable=False),
sa.Column("name", sa.String(length=300), nullable=False),
sa.Column("description", sa.Text(), nullable=True),
sa.Column("status", sa.String(length=32), nullable=False),
sa.Column("current_revision", sa.Integer(), nullable=False),
sa.Column("active_revision", sa.Integer(), nullable=True),
sa.Column("metadata", sa.JSON(), nullable=False),
sa.Column("created_by", sa.String(length=255), nullable=True),
sa.Column("updated_by", sa.String(length=255), nullable=True),
sa.Column("deleted_at", sa.DateTime(timezone=True), nullable=True),
sa.Column("created_at", sa.DateTime(timezone=True), nullable=False),
sa.Column("updated_at", sa.DateTime(timezone=True), nullable=False),
sa.PrimaryKeyConstraint("id", name=op.f("pk_workflow_definitions")),
sa.UniqueConstraint(
"tenant_id",
"definition_key",
name="uq_workflow_definition_key",
),
)
op.create_index(
op.f("ix_workflow_definitions_created_by"),
"workflow_definitions",
["created_by"],
unique=False,
)
op.create_index(
op.f("ix_workflow_definitions_deleted_at"),
"workflow_definitions",
["deleted_at"],
unique=False,
)
op.create_index(
op.f("ix_workflow_definitions_status"),
"workflow_definitions",
["status"],
unique=False,
)
op.create_index(
op.f("ix_workflow_definitions_tenant_id"),
"workflow_definitions",
["tenant_id"],
unique=False,
)
op.create_index(
op.f("ix_workflow_definitions_updated_by"),
"workflow_definitions",
["updated_by"],
unique=False,
)
op.create_index(
"ix_workflow_definitions_tenant_status",
"workflow_definitions",
["tenant_id", "status"],
unique=False,
)
op.create_index(
"ix_workflow_definitions_tenant_updated",
"workflow_definitions",
["tenant_id", "updated_at"],
unique=False,
)
op.create_table(
"workflow_definition_revisions",
sa.Column("id", sa.String(length=36), nullable=False),
sa.Column("tenant_id", sa.String(length=36), nullable=False),
sa.Column("definition_id", sa.String(length=36), nullable=False),
sa.Column("revision", sa.Integer(), nullable=False),
sa.Column("schema_version", sa.Integer(), nullable=False),
sa.Column("graph", sa.JSON(), nullable=False),
sa.Column("content_hash", sa.String(length=64), nullable=False),
sa.Column("library_id", sa.String(length=100), nullable=False),
sa.Column("library_version", sa.String(length=40), nullable=False),
sa.Column("created_by", sa.String(length=255), nullable=True),
sa.Column("created_at", sa.DateTime(timezone=True), nullable=False),
sa.Column("updated_at", sa.DateTime(timezone=True), nullable=False),
sa.ForeignKeyConstraint(
["definition_id"],
["workflow_definitions.id"],
name=op.f(
"fk_workflow_definition_revisions_definition_id_workflow_definitions"
),
ondelete="CASCADE",
),
sa.PrimaryKeyConstraint(
"id",
name=op.f("pk_workflow_definition_revisions"),
),
sa.UniqueConstraint(
"definition_id",
"revision",
name="uq_workflow_definition_revision",
),
)
op.create_index(
op.f("ix_workflow_definition_revisions_created_by"),
"workflow_definition_revisions",
["created_by"],
unique=False,
)
op.create_index(
op.f("ix_workflow_definition_revisions_definition_id"),
"workflow_definition_revisions",
["definition_id"],
unique=False,
)
op.create_index(
op.f("ix_workflow_definition_revisions_tenant_id"),
"workflow_definition_revisions",
["tenant_id"],
unique=False,
)
op.create_index(
"ix_workflow_definition_revisions_content_hash",
"workflow_definition_revisions",
["tenant_id", "content_hash"],
unique=False,
)
op.create_index(
"ix_workflow_definition_revisions_tenant_definition",
"workflow_definition_revisions",
["tenant_id", "definition_id"],
unique=False,
)
def downgrade() -> None:
op.drop_index(
"ix_workflow_definition_revisions_tenant_definition",
table_name="workflow_definition_revisions",
)
op.drop_index(
"ix_workflow_definition_revisions_content_hash",
table_name="workflow_definition_revisions",
)
op.drop_index(
op.f("ix_workflow_definition_revisions_tenant_id"),
table_name="workflow_definition_revisions",
)
op.drop_index(
op.f("ix_workflow_definition_revisions_definition_id"),
table_name="workflow_definition_revisions",
)
op.drop_index(
op.f("ix_workflow_definition_revisions_created_by"),
table_name="workflow_definition_revisions",
)
op.drop_table("workflow_definition_revisions")
op.drop_index(
"ix_workflow_definitions_tenant_updated",
table_name="workflow_definitions",
)
op.drop_index(
"ix_workflow_definitions_tenant_status",
table_name="workflow_definitions",
)
op.drop_index(
op.f("ix_workflow_definitions_updated_by"),
table_name="workflow_definitions",
)
op.drop_index(
op.f("ix_workflow_definitions_tenant_id"),
table_name="workflow_definitions",
)
op.drop_index(
op.f("ix_workflow_definitions_status"),
table_name="workflow_definitions",
)
op.drop_index(
op.f("ix_workflow_definitions_deleted_at"),
table_name="workflow_definitions",
)
op.drop_index(
op.f("ix_workflow_definitions_created_by"),
table_name="workflow_definitions",
)
op.drop_table("workflow_definitions")
@@ -0,0 +1,209 @@
"""v0.1.14 governed Workflow definitions
Revision ID: c6d8f1a3e5b7
Revises: a7c4e2f9b1d3
Create Date: 2026-07-28 00:00:00.000000
"""
from __future__ import annotations
from alembic import op
import sqlalchemy as sa
revision = "c6d8f1a3e5b7"
down_revision = "a7c4e2f9b1d3"
branch_labels = None
depends_on = None
def upgrade() -> None:
with op.batch_alter_table("workflow_definitions") as batch_op:
batch_op.drop_constraint(
"uq_workflow_definition_key",
type_="unique",
)
batch_op.alter_column(
"tenant_id",
existing_type=sa.String(length=36),
nullable=True,
)
batch_op.add_column(
sa.Column(
"scope_type",
sa.String(length=20),
nullable=False,
server_default="tenant",
)
)
batch_op.add_column(
sa.Column("scope_id", sa.String(length=36), nullable=True)
)
batch_op.add_column(
sa.Column(
"scope_key",
sa.String(length=80),
nullable=False,
server_default="tenant:legacy",
)
)
batch_op.add_column(
sa.Column(
"definition_kind",
sa.String(length=20),
nullable=False,
server_default="flow",
)
)
batch_op.add_column(
sa.Column(
"inherit_to_lower_scopes",
sa.Boolean(),
nullable=False,
server_default=sa.false(),
)
)
batch_op.add_column(
sa.Column(
"allow_start",
sa.Boolean(),
nullable=False,
server_default=sa.true(),
)
)
batch_op.add_column(
sa.Column(
"allow_reuse",
sa.Boolean(),
nullable=False,
server_default=sa.false(),
)
)
batch_op.add_column(
sa.Column(
"allow_automation",
sa.Boolean(),
nullable=False,
server_default=sa.false(),
)
)
batch_op.add_column(
sa.Column(
"derived_from_definition_id",
sa.String(length=36),
nullable=True,
)
)
batch_op.add_column(
sa.Column("derived_from_revision", sa.Integer(), nullable=True)
)
batch_op.add_column(
sa.Column(
"derived_from_hash",
sa.String(length=64),
nullable=True,
)
)
batch_op.add_column(
sa.Column(
"derivation_provenance",
sa.JSON(),
nullable=False,
server_default=sa.text("'{}'"),
)
)
op.execute(
sa.text(
"UPDATE workflow_definitions "
"SET scope_id = tenant_id, "
"scope_key = 'tenant:' || tenant_id "
"WHERE tenant_id IS NOT NULL"
)
)
with op.batch_alter_table("workflow_definitions") as batch_op:
batch_op.create_unique_constraint(
"uq_workflow_definition_key",
["scope_key", "definition_key"],
)
for column in (
"scope_type",
"scope_id",
"scope_key",
"definition_kind",
"derived_from_definition_id",
):
op.create_index(
op.f(f"ix_workflow_definitions_{column}"),
"workflow_definitions",
[column],
unique=False,
)
with op.batch_alter_table(
"workflow_definition_revisions"
) as batch_op:
batch_op.alter_column(
"tenant_id",
existing_type=sa.String(length=36),
nullable=True,
)
def downgrade() -> None:
op.execute(
sa.text(
"UPDATE workflow_definition_revisions "
"SET tenant_id = COALESCE(tenant_id, 'system')"
)
)
with op.batch_alter_table(
"workflow_definition_revisions"
) as batch_op:
batch_op.alter_column(
"tenant_id",
existing_type=sa.String(length=36),
nullable=False,
)
for column in (
"derived_from_definition_id",
"definition_kind",
"scope_key",
"scope_id",
"scope_type",
):
op.drop_index(
op.f(f"ix_workflow_definitions_{column}"),
table_name="workflow_definitions",
)
op.execute(
sa.text(
"UPDATE workflow_definitions "
"SET tenant_id = COALESCE(tenant_id, 'system')"
)
)
with op.batch_alter_table("workflow_definitions") as batch_op:
batch_op.drop_constraint(
"uq_workflow_definition_key",
type_="unique",
)
batch_op.create_unique_constraint(
"uq_workflow_definition_key",
["tenant_id", "definition_key"],
)
batch_op.drop_column("derivation_provenance")
batch_op.drop_column("derived_from_hash")
batch_op.drop_column("derived_from_revision")
batch_op.drop_column("derived_from_definition_id")
batch_op.drop_column("allow_automation")
batch_op.drop_column("allow_reuse")
batch_op.drop_column("allow_start")
batch_op.drop_column("inherit_to_lower_scopes")
batch_op.drop_column("definition_kind")
batch_op.drop_column("scope_key")
batch_op.drop_column("scope_id")
batch_op.drop_column("scope_type")
batch_op.alter_column(
"tenant_id",
existing_type=sa.String(length=36),
nullable=False,
)
@@ -0,0 +1,213 @@
"""v0.1.14 Workflow instances and resumable handoffs
Revision ID: d8f2a5c7e1b4
Revises: c6d8f1a3e5b7
Create Date: 2026-07-30 00:00:00.000000
"""
from __future__ import annotations
from alembic import op
import sqlalchemy as sa
revision = "d8f2a5c7e1b4"
down_revision = "c6d8f1a3e5b7"
branch_labels = None
depends_on = None
def upgrade() -> None:
op.create_table(
"workflow_instances",
sa.Column("id", sa.String(length=36), nullable=False),
sa.Column("tenant_id", sa.String(length=36), nullable=False),
sa.Column("definition_id", sa.String(length=36), nullable=False),
sa.Column(
"definition_revision_id",
sa.String(length=36),
nullable=False,
),
sa.Column("status", sa.String(length=30), nullable=False),
sa.Column("idempotency_key", sa.String(length=255), nullable=False),
sa.Column("correlation_id", sa.String(length=128), nullable=True),
sa.Column("current_step_id", sa.String(length=36), nullable=True),
sa.Column("input", sa.JSON(), nullable=False),
sa.Column("context", sa.JSON(), nullable=False),
sa.Column("output", sa.JSON(), nullable=False),
sa.Column("authorization", sa.JSON(), nullable=False),
sa.Column("started_at", sa.DateTime(timezone=True), nullable=False),
sa.Column("finished_at", sa.DateTime(timezone=True), nullable=True),
sa.Column(
"cancellation_requested_at",
sa.DateTime(timezone=True),
nullable=True,
),
sa.Column("error", sa.Text(), nullable=True),
sa.Column("created_by", sa.String(length=255), nullable=True),
sa.Column("created_at", sa.DateTime(timezone=True), nullable=False),
sa.Column("updated_at", sa.DateTime(timezone=True), nullable=False),
sa.ForeignKeyConstraint(
["definition_id"],
["workflow_definitions.id"],
ondelete="CASCADE",
),
sa.ForeignKeyConstraint(
["definition_revision_id"],
["workflow_definition_revisions.id"],
ondelete="RESTRICT",
),
sa.PrimaryKeyConstraint("id"),
sa.UniqueConstraint(
"tenant_id",
"definition_id",
"idempotency_key",
name="uq_workflow_instance_idempotency",
),
)
for column in (
"tenant_id",
"definition_id",
"definition_revision_id",
"status",
"idempotency_key",
"correlation_id",
"current_step_id",
"created_by",
):
op.create_index(
op.f(f"ix_workflow_instances_{column}"),
"workflow_instances",
[column],
unique=False,
)
op.create_index(
"ix_workflow_instances_tenant_status",
"workflow_instances",
["tenant_id", "status"],
unique=False,
)
op.create_index(
"ix_workflow_instances_reconcile",
"workflow_instances",
["status", "updated_at"],
unique=False,
)
op.create_table(
"workflow_instance_steps",
sa.Column("id", sa.String(length=36), nullable=False),
sa.Column("tenant_id", sa.String(length=36), nullable=False),
sa.Column("instance_id", sa.String(length=36), nullable=False),
sa.Column("sequence", sa.Integer(), nullable=False),
sa.Column("node_id", sa.String(length=120), nullable=False),
sa.Column("node_type", sa.String(length=120), nullable=False),
sa.Column("status", sa.String(length=30), nullable=False),
sa.Column("attempt", sa.Integer(), nullable=False),
sa.Column("idempotency_key", sa.String(length=255), nullable=False),
sa.Column("input", sa.JSON(), nullable=False),
sa.Column("output", sa.JSON(), nullable=False),
sa.Column("handoff", sa.JSON(), nullable=False),
sa.Column("external_ref", sa.String(length=500), nullable=True),
sa.Column("started_at", sa.DateTime(timezone=True), nullable=True),
sa.Column("finished_at", sa.DateTime(timezone=True), nullable=True),
sa.Column("error", sa.Text(), nullable=True),
sa.Column("completed_by", sa.String(length=255), nullable=True),
sa.Column("created_at", sa.DateTime(timezone=True), nullable=False),
sa.Column("updated_at", sa.DateTime(timezone=True), nullable=False),
sa.ForeignKeyConstraint(
["instance_id"],
["workflow_instances.id"],
ondelete="CASCADE",
),
sa.PrimaryKeyConstraint("id"),
sa.UniqueConstraint(
"instance_id",
"sequence",
name="uq_workflow_instance_step_sequence",
),
)
for column in (
"tenant_id",
"instance_id",
"node_id",
"node_type",
"status",
"external_ref",
):
op.create_index(
op.f(f"ix_workflow_instance_steps_{column}"),
"workflow_instance_steps",
[column],
unique=False,
)
op.create_index(
"ix_workflow_instance_steps_tenant_status",
"workflow_instance_steps",
["tenant_id", "status"],
unique=False,
)
op.create_table(
"workflow_instance_events",
sa.Column("id", sa.String(length=36), nullable=False),
sa.Column("tenant_id", sa.String(length=36), nullable=False),
sa.Column("instance_id", sa.String(length=36), nullable=False),
sa.Column("step_id", sa.String(length=36), nullable=True),
sa.Column("sequence", sa.Integer(), nullable=False),
sa.Column("kind", sa.String(length=120), nullable=False),
sa.Column("actor_id", sa.String(length=255), nullable=True),
sa.Column("payload", sa.JSON(), nullable=False),
sa.Column("created_at", sa.DateTime(timezone=True), nullable=False),
sa.ForeignKeyConstraint(
["instance_id"],
["workflow_instances.id"],
ondelete="CASCADE",
),
sa.PrimaryKeyConstraint("id"),
sa.UniqueConstraint(
"instance_id",
"sequence",
name="uq_workflow_instance_event_sequence",
),
)
for column in (
"tenant_id",
"instance_id",
"step_id",
"kind",
"actor_id",
):
op.create_index(
op.f(f"ix_workflow_instance_events_{column}"),
"workflow_instance_events",
[column],
unique=False,
)
op.create_index(
"ix_workflow_instance_events_tenant_created",
"workflow_instance_events",
["tenant_id", "created_at"],
unique=False,
)
def downgrade() -> None:
op.drop_index(
"ix_workflow_instance_events_tenant_created",
table_name="workflow_instance_events",
)
op.drop_table("workflow_instance_events")
op.drop_index(
"ix_workflow_instance_steps_tenant_status",
table_name="workflow_instance_steps",
)
op.drop_table("workflow_instance_steps")
op.drop_index(
"ix_workflow_instances_reconcile",
table_name="workflow_instances",
)
op.drop_index(
"ix_workflow_instances_tenant_status",
table_name="workflow_instances",
)
op.drop_table("workflow_instances")
@@ -0,0 +1,68 @@
"""v0.1.14 normalized BPMN revision artifacts
Revision ID: e9a4c6b8d2f1
Revises: d8f2a5c7e1b4
Create Date: 2026-07-30 00:00:00.000000
"""
from __future__ import annotations
from alembic import op
import sqlalchemy as sa
revision = "e9a4c6b8d2f1"
down_revision = "d8f2a5c7e1b4"
branch_labels = None
depends_on = None
def upgrade() -> None:
with op.batch_alter_table("workflow_definition_revisions") as batch_op:
batch_op.add_column(sa.Column("bpmn_xml", sa.Text(), nullable=True))
batch_op.add_column(
sa.Column("bpmn_hash", sa.String(length=64), nullable=True)
)
batch_op.add_column(
sa.Column(
"bpmn_adapter_id",
sa.String(length=120),
nullable=True,
)
)
batch_op.add_column(
sa.Column(
"bpmn_adapter_version",
sa.String(length=40),
nullable=True,
)
)
batch_op.add_column(
sa.Column(
"bpmn_runtime_kind",
sa.String(length=20),
nullable=True,
)
)
batch_op.add_column(
sa.Column("bpmn_executable", sa.Boolean(), nullable=True)
)
op.create_index(
op.f("ix_workflow_definition_revisions_bpmn_hash"),
"workflow_definition_revisions",
["bpmn_hash"],
unique=False,
)
def downgrade() -> None:
op.drop_index(
op.f("ix_workflow_definition_revisions_bpmn_hash"),
table_name="workflow_definition_revisions",
)
with op.batch_alter_table("workflow_definition_revisions") as batch_op:
batch_op.drop_column("bpmn_executable")
batch_op.drop_column("bpmn_runtime_kind")
batch_op.drop_column("bpmn_adapter_version")
batch_op.drop_column("bpmn_adapter_id")
batch_op.drop_column("bpmn_hash")
batch_op.drop_column("bpmn_xml")
@@ -0,0 +1,62 @@
"""v0.1.14 workflow execution modes and focused Views
Revision ID: f1b7d3e5a9c2
Revises: e9a4c6b8d2f1
Create Date: 2026-07-30 00:00:00.000000
"""
from __future__ import annotations
from alembic import op
import sqlalchemy as sa
revision = "f1b7d3e5a9c2"
down_revision = "e9a4c6b8d2f1"
branch_labels = None
depends_on = None
def upgrade() -> None:
with op.batch_alter_table("workflow_definition_revisions") as batch_op:
batch_op.add_column(
sa.Column(
"execution_mode",
sa.String(length=20),
nullable=False,
server_default="hybrid",
)
)
batch_op.add_column(
sa.Column("view_id", sa.String(length=36), nullable=True)
)
batch_op.add_column(
sa.Column(
"view_revision_id",
sa.String(length=36),
nullable=True,
)
)
with op.batch_alter_table("workflow_instances") as batch_op:
batch_op.add_column(
sa.Column(
"start_origin",
sa.String(length=30),
nullable=False,
server_default="user",
)
)
batch_op.create_index(
"ix_workflow_instances_start_origin",
["start_origin"],
unique=False,
)
def downgrade() -> None:
with op.batch_alter_table("workflow_instances") as batch_op:
batch_op.drop_index("ix_workflow_instances_start_origin")
batch_op.drop_column("start_origin")
with op.batch_alter_table("workflow_definition_revisions") as batch_op:
batch_op.drop_column("view_revision_id")
batch_op.drop_column("view_id")
batch_op.drop_column("execution_mode")