Skip to content

Subroutine ​

A subroutine owns a directed graph, its input and output types, parameters, and lexically nested definitions. Its graph combines features, agent resources, nodes, edges, and visits. A subroutine call executes the graph in the caller's process and Python environment.

Authored files ​

Boundary values ​

● impl.py declares the type boundary seen by callers. The generated ◆ __init__.py imports and exposes these types:

TypeRole
InputThe payload entering through the enter port.
OutputThe payload returned through the exit port.
ParamsImmutable configuration for this definition; it does not flow along edges.

The graph connects these boundary types through its generated visit declarations. The behavior of each visit belongs in its authored implementation.

Profiles and sessions may be defined locally or declared as resources that a caller must supply. A generated call-node declaration records the child parameter closure and maps those resource arguments.

Generated interface ​

The subroutine's generated ◆ __init__.py exposes the authored boundary types and the following declarations:

DeclarationRole
GRAPH_IDThe canonical graph identifier.
StateScopeA Literal type containing the graph identifier, used to distinguish state belonging to different subroutines.
NODE_IDS, EDGE_IDS, FEATURE_IDSImmutable records of typed identifiers for directly owned nodes, ports, edges, and features.
PROFILE_IDS, SESSION_IDSImmutable records of typed identifiers for local resources and resource parameters.
definition()A cached factory returning SubroutineDefinition[Input, Output, Params, StateScope].

The factory imports the control-port declarations and component registries inside its body, then assembles a typed GraphDefinition. These imports occur when the factory is first called; the boundary types and identifiers are available before that assembly.

Registries ​

Collection modules import directly owned declarations and export tuples. They do not collect declarations from nested subroutines. Empty collections export (). Ports are assembled separately from executable nodes.

ModuleExports
features/__init__.pyFEATURES
nodes/__init__.pyNODES (executable nodes only)
edges/__init__.pyEDGES
profiles/__init__.pyPROFILES, PROFILE_PARAMETERS
sessions/__init__.pySESSIONS, SESSION_PARAMETERS

EDGES is collected in edge-ID order. This order does not establish routing priority: execution requires exactly one compatible outgoing edge. Nested workflows/__init__.py and subroutines/__init__.py are empty package markers; call declarations name their target modules directly. Workflow resource registries follow the workflow's interface.

Files ​

Each subroutine directory contains a generated declaration, component registries, package markers for nested definitions, and an authored boundary module.

General subroutine structure

src/<package>/subroutines/<subroutine>/
├── ◆ __init__.py
├── ◆ workflows/__init__.py
├── ◆ subroutines/__init__.py
├── ◆ profiles/__init__.py
├── ◆ sessions/__init__.py
├── ◆ features/__init__.py
├── ◆ nodes/__init__.py
├── ◆ edges/__init__.py
└── ● impl.py

Nested subroutines repeat this complete layout under subroutines/<owner>/subroutines/<id>/.

FileRole
◆ __init__.pyExposes boundary types, declares typed identifiers, and assembles ports and component registries in the cached definition() factory.
◆ workflows/__init__.py and ◆ subroutines/__init__.pyEmpty package markers for nested definitions.
Other collection ◆ __init__.py filesCollect directly owned features, profiles, sessions, nodes, and edges.
● impl.pyDeclares the immutable Params type and the subroutine's Input and Output types.

Countdown example ​

Source: ● subroutines/main/impl.py

python
"""Input and output for the Countdown workflow."""

from dataclasses import dataclass


@dataclass(frozen=True, slots=True, kw_only=True)
class Count:
    """A non-negative countdown value; defaults support one-click execution."""

    value: int = 10

    def __post_init__(self) -> None:
        if self.value < 0:
            raise ValueError("the countdown value must be non-negative")


@dataclass(frozen=True, slots=True, kw_only=True)
class Params:
    pass


Input = Count
Output = Count

The Countdown subroutine declares its boundary in this authored module. The same Count type crosses both ports, and Params has no fields. The non-negative check is ordinary authored Python; the annotation value: int does not express that constraint.

Source: ◆ subroutines/main/__init__.py

python
from __future__ import annotations
from dataclasses import dataclass
from functools import cache
from typing import Final, Literal
from verdog_runtime.declarations.ids import (
    EdgeId,
    GraphId,
    NodeId,
    FeatureId,
    AgentProfileId,
    AgentSessionId,
)
from verdog_runtime.declarations import GraphDefinition, SubroutineDefinition
from .impl import Input, Output, Params

GRAPH_ID: Final[GraphId] = GraphId('demo.countdown.main')
type StateScope = Literal['demo.countdown.main']


@dataclass(frozen=True, slots=True, kw_only=True)
class NodeIdentifiers:
    check_counter: NodeId
    decrement: NodeId
    enter: NodeId
    exit: NodeId
    failure: NodeId
    initialize_counter: NodeId
    update_counter: NodeId


NODE_IDS: Final[NodeIdentifiers] = NodeIdentifiers(
    check_counter=NodeId('check_counter'),
    decrement=NodeId('decrement'),
    enter=NodeId('enter'),
    exit=NodeId('exit'),
    failure=NodeId('failure'),
    initialize_counter=NodeId('initialize_counter'),
    update_counter=NodeId('update_counter'),
)


@dataclass(frozen=True, slots=True, kw_only=True)
class EdgeIdentifiers:
    check_counter__decrement: EdgeId
    check_counter__exit: EdgeId
    decrement__update_counter: EdgeId
    enter__initialize_counter: EdgeId
    initialize_counter__check_counter: EdgeId
    update_counter__check_counter: EdgeId


EDGE_IDS: Final[EdgeIdentifiers] = EdgeIdentifiers(
    check_counter__decrement=EdgeId('check_counter__decrement'),
    check_counter__exit=EdgeId('check_counter__exit'),
    decrement__update_counter=EdgeId('decrement__update_counter'),
    enter__initialize_counter=EdgeId('enter__initialize_counter'),
    initialize_counter__check_counter=EdgeId('initialize_counter__check_counter'),
    update_counter__check_counter=EdgeId('update_counter__check_counter'),
)


@dataclass(frozen=True, slots=True, kw_only=True)
class FeatureIdentifiers:
    counter: FeatureId


FEATURE_IDS: Final[FeatureIdentifiers] = FeatureIdentifiers(
    counter=FeatureId('counter'),
)


@dataclass(frozen=True, slots=True, kw_only=True)
class AgentProfileIdentifiers:
    decrementer: AgentProfileId


PROFILE_IDS: Final[AgentProfileIdentifiers] = AgentProfileIdentifiers(
    decrementer=AgentProfileId('decrementer'),
)


@dataclass(frozen=True, slots=True, kw_only=True)
class AgentSessionIdentifiers:
    countdown: AgentSessionId


SESSION_IDS: Final[AgentSessionIdentifiers] = AgentSessionIdentifiers(
    countdown=AgentSessionId('countdown'),
)


@cache
def definition() -> SubroutineDefinition[Input, Output, Params, StateScope]:
    from demo.countdown.subroutines.main.nodes import NODES
    from demo.countdown.subroutines.main.features import FEATURES
    from demo.countdown.subroutines.main.profiles import PROFILES, PROFILE_PARAMETERS
    from demo.countdown.subroutines.main.sessions import SESSIONS, SESSION_PARAMETERS
    from demo.countdown.subroutines.main.nodes.enter import PORT as ENTER
    from demo.countdown.subroutines.main.nodes.exit import PORT as EXIT
    from demo.countdown.subroutines.main.nodes.failure import PORT as FAILURE
    from demo.countdown.subroutines.main.edges import EDGES

    return SubroutineDefinition(
        graph=GraphDefinition[Input, Output, Params, StateScope](
            id=GRAPH_ID,
            params_type=Params,
            enter=ENTER,
            exit=EXIT,
            failure=FAILURE,
            nodes=NODES,
            edges=EDGES,
            features=FEATURES,
            profiles=PROFILES,
            profile_parameters=PROFILE_PARAMETERS,
            sessions=SESSIONS,
            session_parameters=SESSION_PARAMETERS,
        ),
    )

The generated module exposes the authored types and declares typed identifiers for the graph in project.json. StateScope identifies this subroutine's state at the type level.

The cached definition() combines Countdown's ports, executable nodes, edges, features, and agent resources in one graph. The workflow selects this subroutine and supplies its execution environment.

Countdown's feature registry contains the counter declaration. The factory above imports its FEATURES tuple when assembling the graph.