orchestrata_ultimate.orchestrator.intent_workflow
Reference › orchestrata_ultimate › orchestrator › intent_workflow
A small engine for running several intents as one workflow. No other reference page imports it. It builds on the IntentLifecycleManager for status changes and on RedisStateManager for workflow state, and its steps hold Intent records.
A WorkflowStep has an id, an intent, depends_on ids, a FailureStrategy (retry, rollback, continue or fail; the default is fail), max_retries (3) and timeout_seconds (300). IntentWorkflow.define builds a WorkflowDef with an execution mode, and execute marks the workflow running in Redis, then runs it sequentially, in parallel, or as a DAG, and records the final status and duration. An unknown mode fails the result. The DAG mode repeatedly picks steps whose dependencies have completed and fails with "Deadlock: no ready steps" if none is ready; a failed step either stops the workflow (FAIL) or is treated as done (CONTINUE). _execute_step stores the step's intent and walks it through the lifecycle statuses to COMPLETED using the lifecycle manager, or to FAILED on error; _retry_step handles retries. Control methods are cancel, pause, resume, get_status and list_workflows, which can filter by WorkflowStatus. The result is a WorkflowResult with per-step results, error, timestamps and duration.
At a glance
| Source | src/orchestrata_ultimate/orchestrator/intent_workflow.py |
| Lines of code | 494 |
| Classes | 6 |
| Functions | 0 |
| Imports (internal) | orchestrata_ultimate.core.base.traces, orchestrata_ultimate.infra.redis_state, orchestrata_ultimate.orchestrator.intent_lifecycle, orchestrata_ultimate.orchestrator.intent_types |
| Imported by | none |
| Tests that import it | tests/generated_orchestrator/test_intent_workflow_generated.py, tests/orchestrator/test_intent_workflow_behavioral.py |
Class diagram
Dependency diagram
API
Classes
WorkflowStatus (enum)
Bases: StrEnum · Decorators: traces(spec='003', story='US-008', reqs=['REQ-003-US-008-007']) · Line 25
Workflow execution status.
| Member | Value |
|---|---|
PENDING |
'pending' |
RUNNING |
'running' |
PAUSED |
'paused' |
COMPLETED |
'completed' |
FAILED |
'failed' |
CANCELLED |
'cancelled' |
FailureStrategy (enum)
Bases: StrEnum · Decorators: traces(spec='003', story='US-008', reqs=['REQ-003-US-008-004', 'REQ-003-US-008-016']) · Line 37
How to handle step failure.
| Member | Value |
|---|---|
RETRY |
'retry' |
ROLLBACK |
'rollback' |
CONTINUE |
'continue' |
FAIL |
'fail' |
WorkflowStep (dataclass)
Decorators: traces(spec='003', story='US-008', reqs=['REQ-003-US-008-001', 'REQ-003-US-008-012']), dataclass · Line 53
A single step in a workflow.
| Attribute | Type | Default |
|---|---|---|
id |
str |
|
intent |
Intent |
|
depends_on |
list[str] |
field(default_factory=list) |
failure_strategy |
FailureStrategy |
FailureStrategy.FAIL |
max_retries |
int |
3 |
timeout_seconds |
int |
300 |
retry_count |
int |
0 |
| Method | Signature | Summary |
|---|---|---|
to_dict |
to_dict() -> dict[str, Any] |
WorkflowDef (dataclass)
Decorators: traces(spec='003', story='US-008', reqs=['REQ-003-US-008-001', 'REQ-003-US-008-012']), dataclass · Line 79
Workflow definition with steps and execution mode.
| Attribute | Type | Default |
|---|---|---|
id |
str |
|
steps |
list[WorkflowStep] |
|
execution_mode |
str |
'sequential' |
created_at |
datetime |
field(default_factory=lambda: datetime.now(UTC)) |
| Method | Signature | Summary |
|---|---|---|
to_dict |
to_dict() -> dict[str, Any] |
|
get_step |
get_step(step_id: str) -> WorkflowStep | None |
WorkflowResult (dataclass)
Decorators: traces(spec='003', story='US-008', reqs=['REQ-003-US-008-010', 'REQ-003-US-008-022']), dataclass · Line 106
Result of workflow execution.
| Attribute | Type | Default |
|---|---|---|
workflow_id |
str |
|
status |
WorkflowStatus |
|
step_results |
dict[str, str] |
field(default_factory=dict) |
started_at |
datetime | None |
None |
completed_at |
datetime | None |
None |
error |
str | None |
None |
duration_ms |
float |
0.0 |
| Method | Signature | Summary |
|---|---|---|
to_dict |
to_dict() -> dict[str, Any] |
IntentWorkflow
Decorators: traces(spec='003', story='US-008', reqs=['REQ-003-US-008-002', 'REQ-003-US-008-003', 'REQ-003-US-008-004', 'REQ-003-US-… · Line 143
Workflow engine for multi-step intent execution.
| Method | Signature | Summary |
|---|---|---|
__init__ |
__init__(lifecycle: IntentLifecycleManager, redis: RedisStateManager) -> None |
|
client property |
client() -> Any |
|
define |
async define(steps: list[WorkflowStep], mode: str = 'sequential') -> WorkflowDef |
Define a new workflow. |
execute |
async execute(workflow_def: WorkflowDef) -> WorkflowResult |
Execute a workflow definition. |
_execute_sequential |
async _execute_sequential(workflow_def: WorkflowDef, result: WorkflowResult) -> WorkflowResult |
Execute steps sequentially. |
_execute_parallel |
async _execute_parallel(workflow_def: WorkflowDef, result: WorkflowResult) -> WorkflowResult |
Execute all steps (no dependency ordering). |
_execute_dag |
async _execute_dag(workflow_def: WorkflowDef, result: WorkflowResult) -> WorkflowResult |
Execute steps respecting dependency graph. |
_execute_step |
async _execute_step(step: WorkflowStep) -> str |
Execute a single workflow step via the lifecycle manager. |
_retry_step |
async _retry_step(step: WorkflowStep) -> str |
Retry a failed step up to max_retries. |
cancel |
async cancel(workflow_id: str) -> None |
Cancel a running workflow. |
get_status |
async get_status(workflow_id: str) -> WorkflowResult | None |
Get current workflow status. |
pause |
async pause(workflow_id: str) -> None |
Pause a running workflow. |
resume |
async resume(workflow_id: str) -> None |
Resume a paused workflow. |
list_workflows |
async list_workflows(status: WorkflowStatus | None = None) -> list[str] |
List workflow IDs. |
Source
"""Intent workflow engine for multi-step intent execution.
Supports sequential, parallel, and DAG-based execution modes
with failure strategies, retries, and state persistence.
"""
import json
import logging
import time
import uuid
from dataclasses import dataclass, field
from datetime import UTC, datetime
from enum import StrEnum
from typing import Any
from orchestrata_ultimate.core.base.traces import traces
from orchestrata_ultimate.infra.redis_state import RedisStateManager
from orchestrata_ultimate.orchestrator.intent_lifecycle import IntentLifecycleManager
from orchestrata_ultimate.orchestrator.intent_types import Intent, IntentStatus
logger = logging.getLogger(__name__)
@traces(spec="003", story="US-008", reqs=["REQ-003-US-008-007"])
class WorkflowStatus(StrEnum):
"""Workflow execution status."""
PENDING = "pending"
RUNNING = "running"
PAUSED = "paused"
COMPLETED = "completed"
FAILED = "failed"
CANCELLED = "cancelled"
@traces(spec="003", story="US-008", reqs=["REQ-003-US-008-004", "REQ-003-US-008-016"])
class FailureStrategy(StrEnum):
"""How to handle step failure."""
RETRY = "retry"
ROLLBACK = "rollback"
CONTINUE = "continue"
FAIL = "fail"
# Redis key prefixes
WORKFLOW_KEY = "orchestrata:workflow"
WORKFLOW_TTL = 86400 # 24 hours
@traces(spec="003", story="US-008", reqs=["REQ-003-US-008-001", "REQ-003-US-008-012"])
@dataclass
class WorkflowStep:
"""A single step in a workflow."""
id: str
intent: Intent
depends_on: list[str] = field(default_factory=list)
failure_strategy: FailureStrategy = FailureStrategy.FAIL
max_retries: int = 3
timeout_seconds: int = 300
retry_count: int = 0
@traces(spec="003", story="US-008", reqs=["REQ-003-US-008-022"])
def to_dict(self) -> dict[str, Any]:
return {
"id": self.id,
"intent_id": self.intent.id,
"depends_on": self.depends_on,
"failure_strategy": self.failure_strategy.value,
"max_retries": self.max_retries,
"timeout_seconds": self.timeout_seconds,
"retry_count": self.retry_count,
}
@traces(spec="003", story="US-008", reqs=["REQ-003-US-008-001", "REQ-003-US-008-012"])
@dataclass
class WorkflowDef:
"""Workflow definition with steps and execution mode."""
id: str
steps: list[WorkflowStep]
execution_mode: str = "sequential" # "sequential", "parallel", "dag"
created_at: datetime = field(default_factory=lambda: datetime.now(UTC))
@traces(spec="003", story="US-008", reqs=["REQ-003-US-008-022"])
def to_dict(self) -> dict[str, Any]:
return {
"id": self.id,
"steps": [s.to_dict() for s in self.steps],
"execution_mode": self.execution_mode,
"created_at": self.created_at.isoformat(),
}
@traces(spec="003", story="US-008", reqs=["REQ-003-US-008-012"])
def get_step(self, step_id: str) -> WorkflowStep | None:
for s in self.steps:
if s.id == step_id:
return s
return None
@traces(spec="003", story="US-008", reqs=["REQ-003-US-008-010", "REQ-003-US-008-022"])
@dataclass
class WorkflowResult:
"""Result of workflow execution."""
workflow_id: str
status: WorkflowStatus
step_results: dict[str, str] = field(default_factory=dict)
started_at: datetime | None = None
completed_at: datetime | None = None
error: str | None = None
duration_ms: float = 0.0
@traces(spec="003", story="US-008", reqs=["REQ-003-US-008-022"])
def to_dict(self) -> dict[str, Any]:
return {
"workflow_id": self.workflow_id,
"status": self.status.value,
"step_results": self.step_results,
"started_at": self.started_at.isoformat() if self.started_at else None,
"completed_at": (
self.completed_at.isoformat() if self.completed_at else None
),
"error": self.error,
"duration_ms": self.duration_ms,
}
@traces(
spec="003",
story="US-008",
reqs=[
"REQ-003-US-008-002",
"REQ-003-US-008-003",
"REQ-003-US-008-004",
"REQ-003-US-008-005",
"REQ-003-US-008-011",
],
)
class IntentWorkflow:
"""Workflow engine for multi-step intent execution.
Manages workflow lifecycle, step ordering, failure handling,
and state persistence via Redis.
"""
def __init__(
self,
lifecycle: IntentLifecycleManager,
redis: RedisStateManager,
) -> None:
self._lifecycle = lifecycle
self._redis = redis
@property
def client(self) -> Any:
return self._redis.client
@traces(spec="003", story="US-008", reqs=["REQ-003-US-008-026"])
async def define(
self,
steps: list[WorkflowStep],
mode: str = "sequential",
) -> WorkflowDef:
"""Define a new workflow."""
workflow = WorkflowDef(
id=str(uuid.uuid4()),
steps=steps,
execution_mode=mode,
)
# Persist workflow definition
self.client.hset(
f"{WORKFLOW_KEY}:{workflow.id}:def",
mapping={"data": json.dumps(workflow.to_dict())},
)
self.client.expire(f"{WORKFLOW_KEY}:{workflow.id}:def", WORKFLOW_TTL)
# Initialize state
self.client.hset(
f"{WORKFLOW_KEY}:{workflow.id}:state",
mapping={
"status": WorkflowStatus.PENDING.value,
"current_step": "",
"started_at": "",
},
)
self.client.expire(f"{WORKFLOW_KEY}:{workflow.id}:state", WORKFLOW_TTL)
return workflow
@traces(
spec="003", story="US-008", reqs=["REQ-003-US-008-032", "REQ-003-US-008-035"]
)
async def execute(self, workflow_def: WorkflowDef) -> WorkflowResult:
"""Execute a workflow definition."""
start_time = time.time()
now = datetime.now(UTC)
result = WorkflowResult(
workflow_id=workflow_def.id,
status=WorkflowStatus.RUNNING,
started_at=now,
)
# Update state to RUNNING
self.client.hset(
f"{WORKFLOW_KEY}:{workflow_def.id}:state",
mapping={
"status": WorkflowStatus.RUNNING.value,
"started_at": now.isoformat(),
},
)
if workflow_def.execution_mode == "sequential":
result = await self._execute_sequential(workflow_def, result)
elif workflow_def.execution_mode == "parallel":
result = await self._execute_parallel(workflow_def, result)
elif workflow_def.execution_mode == "dag":
result = await self._execute_dag(workflow_def, result)
else:
result.status = WorkflowStatus.FAILED
result.error = f"Unknown execution mode: {workflow_def.execution_mode}"
result.completed_at = datetime.now(UTC)
result.duration_ms = (time.time() - start_time) * 1000
# Persist final state
self.client.hset(
f"{WORKFLOW_KEY}:{workflow_def.id}:state",
mapping={
"status": result.status.value,
"completed_at": result.completed_at.isoformat(),
"duration_ms": str(result.duration_ms),
},
)
return result
@traces(spec="003", story="US-008", reqs=["REQ-003-US-008-027"])
async def _execute_sequential(
self, workflow_def: WorkflowDef, result: WorkflowResult
) -> WorkflowResult:
"""Execute steps sequentially."""
for step in workflow_def.steps:
# Update current step
self.client.hset(
f"{WORKFLOW_KEY}:{workflow_def.id}:state",
"current_step",
step.id,
)
step_result = await self._execute_step(step)
result.step_results[step.id] = step_result
if step_result in (
IntentStatus.FAILED.value,
IntentStatus.TIMED_OUT.value,
):
if step.failure_strategy == FailureStrategy.FAIL:
result.status = WorkflowStatus.FAILED
result.error = f"Step {step.id} failed"
return result
elif step.failure_strategy == FailureStrategy.RETRY:
retried = await self._retry_step(step)
result.step_results[step.id] = retried
if retried in (
IntentStatus.FAILED.value,
IntentStatus.TIMED_OUT.value,
):
result.status = WorkflowStatus.FAILED
result.error = f"Step {step.id} failed after retries"
return result
elif step.failure_strategy == FailureStrategy.CONTINUE:
continue
result.status = WorkflowStatus.COMPLETED
return result
@traces(
spec="003",
story="US-008",
reqs=["REQ-003-US-008-003", "REQ-003-US-008-015", "REQ-003-US-008-028"],
)
async def _execute_parallel(
self, workflow_def: WorkflowDef, result: WorkflowResult
) -> WorkflowResult:
"""Execute all steps (no dependency ordering)."""
for step in workflow_def.steps:
step_result = await self._execute_step(step)
result.step_results[step.id] = step_result
all_ok = all(
v == IntentStatus.COMPLETED.value for v in result.step_results.values()
)
result.status = WorkflowStatus.COMPLETED if all_ok else WorkflowStatus.FAILED
if not all_ok:
failed = [
k
for k, v in result.step_results.items()
if v != IntentStatus.COMPLETED.value
]
result.error = f"Steps failed: {', '.join(failed)}"
return result
@traces(
spec="003",
story="US-008",
reqs=[
"REQ-003-US-008-002",
"REQ-003-US-008-008",
"REQ-003-US-008-013",
"REQ-003-US-008-014",
"REQ-003-US-008-020",
"REQ-003-US-008-033",
],
)
async def _execute_dag(
self, workflow_def: WorkflowDef, result: WorkflowResult
) -> WorkflowResult:
"""Execute steps respecting dependency graph."""
completed_steps = set()
remaining = list(workflow_def.steps)
while remaining:
ready = [
s
for s in remaining
if all(dep in completed_steps for dep in s.depends_on)
]
if not ready:
result.status = WorkflowStatus.FAILED
result.error = "Deadlock: no ready steps"
return result
for step in ready:
step_result = await self._execute_step(step)
result.step_results[step.id] = step_result
if step_result == IntentStatus.COMPLETED.value:
completed_steps.add(step.id)
elif step.failure_strategy == FailureStrategy.FAIL:
result.status = WorkflowStatus.FAILED
result.error = f"Step {step.id} failed"
return result
elif step.failure_strategy == FailureStrategy.CONTINUE:
completed_steps.add(step.id)
remaining.remove(step)
result.status = WorkflowStatus.COMPLETED
return result
@traces(spec="003", story="US-008", reqs=["REQ-003-US-008-014"])
async def _execute_step(self, step: WorkflowStep) -> str:
"""Execute a single workflow step via the lifecycle manager."""
try:
self._lifecycle.store_intent(step.intent)
self._lifecycle.transition(step.intent, IntentStatus.VALIDATED)
self._lifecycle.transition(step.intent, IntentStatus.ROUTING)
self._lifecycle.transition(step.intent, IntentStatus.DISPATCHED)
self._lifecycle.transition(step.intent, IntentStatus.EXECUTING)
self._lifecycle.transition(step.intent, IntentStatus.COMPLETED)
return IntentStatus.COMPLETED.value
except Exception as exc:
try:
self._lifecycle.transition(
step.intent, IntentStatus.FAILED, reason=str(exc)
)
except Exception as transition_exc:
logger.warning(
"Failed to transition step intent %s to FAILED: %s",
getattr(step.intent, "id", "unknown"),
transition_exc,
)
return IntentStatus.FAILED.value
@traces(
spec="003",
story="US-008",
reqs=[
"REQ-003-US-008-004",
"REQ-003-US-008-009",
"REQ-003-US-008-016",
"REQ-003-US-008-021",
"REQ-003-US-008-029",
"REQ-003-US-008-034",
],
)
async def _retry_step(self, step: WorkflowStep) -> str:
"""Retry a failed step up to max_retries."""
for attempt in range(step.max_retries):
step.retry_count += 1
result = await self._execute_step(step)
if result == IntentStatus.COMPLETED.value:
return result
return IntentStatus.FAILED.value
@traces(
spec="003",
story="US-008",
reqs=["REQ-003-US-008-006", "REQ-003-US-008-018", "REQ-003-US-008-031"],
)
async def cancel(self, workflow_id: str) -> None:
"""Cancel a running workflow."""
self.client.hset(
f"{WORKFLOW_KEY}:{workflow_id}:state",
"status",
WorkflowStatus.CANCELLED.value,
)
@traces(
spec="003",
story="US-008",
reqs=[
"REQ-003-US-008-005",
"REQ-003-US-008-007",
"REQ-003-US-008-017",
"REQ-003-US-008-019",
"REQ-003-US-008-030",
],
)
async def get_status(self, workflow_id: str) -> WorkflowResult | None:
"""Get current workflow status."""
raw = self.client.hgetall(f"{WORKFLOW_KEY}:{workflow_id}:state")
if not raw:
return None
def decode(v: Any) -> str:
return v.decode() if isinstance(v, bytes) else v
data = {decode(k): decode(v) for k, v in raw.items()}
status = WorkflowStatus(data.get("status", "pending"))
return WorkflowResult(
workflow_id=workflow_id,
status=status,
started_at=(
datetime.fromisoformat(data["started_at"])
if data.get("started_at")
else None
),
completed_at=(
datetime.fromisoformat(data["completed_at"])
if data.get("completed_at")
else None
),
duration_ms=float(data.get("duration_ms", 0)),
)
@traces(spec="001", story="US-006", reqs=["REQ-001-US-006-016"])
async def pause(self, workflow_id: str) -> None:
"""Pause a running workflow."""
self.client.hset(
f"{WORKFLOW_KEY}:{workflow_id}:state",
"status",
WorkflowStatus.PAUSED.value,
)
@traces(spec="001", story="US-006", reqs=["REQ-001-US-006-016"])
async def resume(self, workflow_id: str) -> None:
"""Resume a paused workflow."""
self.client.hset(
f"{WORKFLOW_KEY}:{workflow_id}:state",
"status",
WorkflowStatus.RUNNING.value,
)
@traces(spec="003", story="US-008", reqs=["REQ-003-US-008-023"])
async def list_workflows(self, status: WorkflowStatus | None = None) -> list[str]:
"""List workflow IDs. Scans Redis keys."""
pattern = f"{WORKFLOW_KEY}:*:state"
ids = []
cursor = 0
while True:
cursor, keys = self.client.scan(cursor, match=pattern, count=100)
for key in keys:
key_str = key.decode() if isinstance(key, bytes) else key
wf_id = key_str.split(":")[2]
if status is not None:
raw_status = self.client.hget(key_str, "status")
if raw_status:
s = (
raw_status.decode()
if isinstance(raw_status, bytes)
else raw_status
)
if s != status.value:
continue
ids.append(wf_id)
if cursor == 0:
break
return ids