Skip to content

Inline Workflows: Encapsulating Workflows Inside Classes

AmritaSense workflows are typically composed from top-level @Node() functions and executed via a standalone WorkflowInterpreter. But in real-world applications, you often want to encapsulate an entire workflow inside a class — keeping nodes, composition, rendering, and execution all in one reusable unit.

This pattern is called an inline workflow.

LangGraph-style alternative

If you're familiar with LangGraph, inline workflows offer a similar programming experience — Python classes encapsulate graph structures, instance methods serve as nodes, and state lives on self. The key differences: AmritaSense skips the StateGraph / add_node / add_edge builder pattern in favor of the >> operator, DI automatically feeds inter-node data, and rendering is a single .render() call.

Why Inline Workflows?

Free-function workflowInline workflow
Nodes are module-level functionsNodes are instance methods of a class
State flows through node outputsState lives naturally on self
Composition and interpreter managed externallyBoth created and stored inside the class
One-off or global useInstantiate, configure, run — like any Python object

Inline workflows give you a clean, self-contained unit that can accept constructor parameters, hold mutable fields, and expose a simple run() method.

Core Design

Three rules define the pattern:

  1. Decorate instance methods with @Node() — they become composable workflow nodes. self is automatically injected by Python's method binding and does not appear in the DI signature.
  2. Compose in __init__ — use >> to chain nodes together, store the composition as an instance attribute.
  3. Render and create the interpreter in __init__ — call .render() and construct WorkflowInterpreter, storing it for later execution.

Simplified Example

python
from amrita_sense.node.core import Node
from amrita_sense.runtime.workflow import WorkflowInterpreter

class SimpleWorkflow:
    """A self-contained workflow: double a value, then format the result."""

    def __init__(self, value: int):
        self.value = value
        self.result: str | None = None

        # Compose, render, create interpreter — all in one place
        rendered = (self.double >> self.format).render()
        self.interpreter = WorkflowInterpreter(rendered)

    @Node()
    async def double(self) -> int:
        """Double the value stored on self."""
        self.value *= 2
        return self.value

    @Node()
    async def format(self) -> str:
        """Format the doubled value from self.value."""
        self.result = f"processed: {self.value}"
        return self.result

    async def run(self) -> str | None:
        await self.interpreter.run()
        return self.result

Usage

python
wf = SimpleWorkflow(value=21)
result = await wf.run()
print(result)  # "processed: 42"

Key Points

self is automatic

@Node() decorates instance methods normally. Python's method binding injects self before the function is called — it never appears in the DI dependency resolution. You don't need extra_args or extra_kwargs just to pass self into your nodes.

Class fields as shared state

Nodes read and write self.xxx directly. Node return values do not automatically flow into the next node's DI context — use instance fields on self to share state across nodes.

Real-World Example

AmritaCore's ChatObject is a production-grade implementation of the inline workflow pattern: it extends SuspendObjectStream, decorates over a dozen instance methods with @Node() in __init__ (_render_train, _limiting_memory, _prepare_messages, _call_completion, etc.), chains them into a full pipeline via >>, renders and creates the interpreter — all state lives naturally on self. The SimpleWorkflow above is the simplified core of that design.

When to Use (and When Not To)

Good fits for inline workflows

ScenarioNotes
Reusable, configurable workflow unitConstructor accepts parameters; instantiate and run
Shared mutable state across nodesself is the natural state container
Library-style APIExpose clear methods like run() / resume() / terminate()
Dynamic compositionSelect node combinations at __init__ time based on constructor args
LangGraph migrationIf your project already uses class-based graph definitions, inline workflows are the closest migration path

When to avoid inline workflows

ScenarioRecommendation
One-off script with 2-3 nodesTop-level >> composition is faster; no class boilerplate
No shared state between nodesFree-function workflows are simpler — each node only cares about inputs and outputs
Cross-module composition neededFree-function workflows naturally support cross-file imports and mixing
Team unfamiliar with OOP patternsInline workflows rely on an understanding of self and method binding
Ultra-high-frequency invocation loopsWhile class instantiation overhead is small, consider it at million-call-per-second scales

Apache 2.0 License