Update: I have abandoned using Absurd. It's buggy and the maintainers are not very welcoming to contributions. I am building my own.
The Integration Model
This document outlines a strategy for merging highway_dsl and Absurd. These two systems are not competitors; they are complementary components operating at different layers of abstraction.
-
highway_dslis a Declarative Workflow Definition Layer. It provides a Python-native (and YAML-serializable) Domain Specific Language (DSL) to define complex workflow graphs. It specifies the "what": the tasks, their dependencies, conditional branches, parallel execution paths, and retry policies. It is the blueprint of the workflow. -
Absurdis an Imperative Durable Execution Runtime. It provides a minimal, Postgres-based SDK to execute long-running functions reliably. It specifies the "how": durable steps, checkpointing of state, crash resilience, and asynchronous primitives (sleeps, events). It is the durable factory worker.
The integration strategy is to use Absurd as the durable execution engine for a workflow interpreter that runs graphs defined by highway_dsl.
The Core Pattern:
- A workflow is defined using
highway_dsl's Python API or loaded from a YAML file. - This workflow definition (the graph object or its serialized YAML/JSON representation) is passed as the initial parameter to a single
Absurdtask. - This
Absurdtask is a generic workflow interpreter. It walks thehighway_dslgraph node by node. - Each node in the
highway_dslgraph (e.g.,Task,Conditional,Parallel) is mapped to a corresponding set ofAbsurdSDK calls (e.g.,ctx.step,ctx.waitForEvent,absurd.spawn).
This model provides a clean separation of concerns: highway_dsl handles the business logic and flow definition, while Absurd handles the state persistence, checkpointing, and resilience.
2. WHAT
highway_dsl (The "Blueprint")
highway_dsl excels at defining workflow structure.
- Graph-Based: Workflows are Directed Acyclic Graphs (DAGs) composed of nodes like
Task,Conditional,Parallel, andStart/End. - Declarative: The developer declares the flow, rather than writing the imperative logic to glue it together. This is evident in its fluent Python API and YAML serialization (
tests/data/car_factory_workflow.yaml). - Rich Flow Control: It natively supports complex patterns:
- Dependencies: Tasks run based on the completion of upstream tasks.
- Conditionals:
if-then-elselogic for branching. - Parallelism: A
Parallelnode to execute multiple branches concurrently. - Retries: A
RetryPolicycan be attached to tasks, definingmax_attempts,delay, andbackoff(tests/test_workflow_dsl.py).
- State Management: The DSL manages the passing of state (outputs from one task to inputs of another) within its in-memory execution model.
Absurd (The "Runtime, aka: HOW")
Based on its provided summary and SDK, Absurd excels at executing code durably.
- Imperative & Step-Based: Execution is a standard function that calls
await ctx.step(...)for any operation that needs to be durable and idempotent. - Durable Checkpoints: The return value of every
ctx.step()is serialized to Postgres. Upon a restart, the function re-runs, butctx.step()calls for completed steps simply return the checkpointed value without re-executing the code. - Resilience (Runs): A task failure (or worker crash) results in a new "Run." This new run replays events and checkpoints, resuming execution at the last failed step.
- Asynchronous Primitives: It provides built-in durable primitives that an in-memory executor lacks:
ctx.sleep(duration): Suspends the task durably.ctx.waitForEvent(name, timeout): Suspends the task until an external event is emitted viaabsurd.emitEvent().
- Dynamic Workflows: Natively supports iterative, self-defining workflows (like AI agents) by versioning step names (e.g.,
iteration,iteration#2,iteration#3).
3. Feature Mapping and Integration Strategy
This section details how each highway_dsl feature is implemented using the Absurd runtime.
Basic Task Execution & State
highway_dsl: ATasknode with an associated function.Absurd: Actx.step(name, func)call.- Integration: The interpreter, upon reaching a
Tasknode, executes it within actx.step. The task's unique name from thehighway_dslgraph is used as theAbsurdstep name.
Example (Pseudo-code for the interpreter):
// Inside Absurd task: run_highway_workflow(params, ctx)
// params.workflowGraph = the highway_dsl YAML/JSON
// params.taskFunctions = a map of task_name -> actual code
async function executeTask(node) {
const taskFunction = params.taskFunctions[node.name];
// The 'highway_dsl' task is executed as a durable 'Absurd' step
const result = await ctx.step(node.name, async () => {
// Load inputs from predecessor tasks
const inputs = loadInputsForNode(node, ctx.checkpoints);
return await taskFunction(inputs);
});
// The result is now in ctx.checkpoints for future steps
return result;
}
Retry Policy
This is a key area of feature overlap with two distinct approaches.
highway_dsl: Defines granular, application-level retry policies (e.g.,max_attempts=3,backoff=2.0) on a per-task basis. This is for transient errors (e.g., API rate limit).Absurd: Defines a task-levelmaxAttemptsonabsurd.spawn(). This retries the entire workflow from the last failed step. This is for catastrophic errors (e.g., worker crash, database connection loss).
Integration Recommendation: Use both for their intended purpose.
highway_dslRetries (Insidectx.step): The application-level retry logic defined inhighway_dslshould be executed inside thectx.stepcall. This handles transient failures without failing theAbsurdstep.AbsurdRetries (Outsidectx.step): TheAbsurdmaxAttemptsonspawnis the "outer loop" of resilience, protecting against infrastructure failure.
Example (Pseudo-code for the interpreter):
async function executeTaskWithHighwayRetry(node) {
const taskFunction = params.taskFunctions[node.name];
const retryPolicy = node.retryPolicy; // { max_attempts: 3, ... }
const result = await ctx.step(node.name, async () => {
// Implement highway_dsl's retry policy *inside* the durable step
let attempts = 0;
while (true) {
try {
attempts++;
const inputs = loadInputsForNode(node, ctx.checkpoints);
return await taskFunction(inputs); // Run the actual business logic
} catch (e) {
if (attempts >= retryPolicy.max_attempts) {
throw e; // Final failure, this fails the ctx.step
}
// Wait using highway_dsl's backoff strategy
await nonDurableSleep(retryPolicy.delay * (retryPolicy.backoff ** attempts));
}
}
});
return result;
}
If ctx.step finally fails after highway_dsl's retries are exhausted, Absurd's mechanism takes over, potentially re-running the entire workflow in a new Run.
Conditional Branching (Conditional)
highway_dsl: AConditionalnode that points to a "condition task," a "then branch," and an "else branch."Absurd: A standard imperativeif/elsestatement.- Integration: The interpreter executes the condition task as a
ctx.step, retrieves its result, and then imperatively follows the correct branch of thehighway_dslgraph.
Example (Pseudo-code for the interpreter):
async function executeConditional(node) {
// 1. Execute the condition task durably
const conditionResult = await ctx.step(node.conditionTask.name, ...);
// 2. Use the result in a standard 'if'
if (conditionResult.value === true) {
await executeNode(node.thenBranch); // Recursively call interpreter
} else {
await executeNode(node.elseBranch); // Recursively call interpreter
}
}
Parallel Execution (Parallel)
This is the most complex integration, as Absurd's ctx.step model is linear. A single task cannot await multiple ctx.step calls concurrently.
highway_dsl: AParallelnode with a list of branches to execute concurrently.Absurd: Noctx.parallel. Parallelism is achieved byspawningnew, independent tasks (absurd.spawn) and coordinating via events (absurd.emitEvent/ctx.waitForEvent).- Integration: The
Parallelnode must be mapped to a "fork/join" pattern usingAbsurd's primitives.
Integration Strategy (Fork/Join):
- Fork: The interpreter, upon hitting a
Parallelnode, executes a singlectx.step(e.g., "fork_parallel_tasks"). Inside this step, it callsabsurd.spawnfor each branch of thehighway_dslParallelnode. These new tasks run a differentAbsurdtask (e.g.,run_highway_sub_workflow). It collects thetaskIds and a uniquejoinEventName. - Join: The main interpreter task then immediately calls
await ctx.waitForEvent(joinEventName, ...)to durably wait for all forked tasks to complete. - Sub-Tasks: The
run_highway_sub_workflowtasks execute their branch. Upon completion, theyawait absurd.emitEvent(joinEventName, { ...result })to signal the main interpreter. - A separate aggregation mechanism is needed to gather all event results before the
joinEventNameis considered fully "met."
Agentic/Dynamic Loops
highway_dsl: Can define an agentic workflow (e.g.,tests/data/complex_agentic_workflow.yaml), but the graph structure is likely static (e.g., a loop from "critique" back to "research").Absurd: Natively supports dynamic loops via step name versioning (iteration->iteration#2).- Integration: This provides a powerful "nested" durability model. A single
highway_dslTask(e.g., "run_agent_loop") can be implemented as anAbsurdtask that itself usesAbsurd's dynamicctx.step("iteration", ...)loop. This allows a structuredhighway_dslgraph to orchestrate highly dynamic, stateful sub-processes.
Asynchronous Primitives (Sleep, Events)
highway_dsl lacks these, but Absurd provides them. This integration adds new, durable capabilities to highway_dsl.
- Integration: Create new
highway_dslnode types, such asSleepTaskandWaitForEventTask. SleepTask: The interpreter maps this node toawait ctx.sleep(node.duration). A workflow can now declaratively "wait 7 days."WaitForEventTask: The interpreter maps this toawait ctx.waitForEvent(node.eventName). A workflow can now declaratively "wait for user_confirmation event."
4. Conclusion: Benefits of the Merged Architecture
This "Declarative Graph + Durable Interpreter" model provides the best of both worlds:
- Separation of Concerns: Business logic (the flow) is defined in
highway_dsl, making it easy to read, test, and modify without touching the execution runtime. Execution resilience is handled entirely byAbsurd. - Full Durability: The entire workflow state, including the current position in the graph (implied by the last
ctx.stepcheckpoint), is durably stored in Postgres. - Enhanced Capabilities:
highway_dslgains powerful, real-world asynchronous capabilities (durable sleeps, external events) by leveraging theAbsurdruntime, features that an in-memory graph executor cannot provide. - Structured Complexity:
Absurdgains a high-level, structured, and serializable framework for defining complex processes, moving beyond simple, linear imperative scripts.