Appearance
Node
An executable node declares an operation performed by its incoming-edge visits. Non-Feature nodes own an output type and local state shared across those visits. Feature nodes update feature state while the runtime forwards their input payload unchanged.
Node kinds
| Kind | Structural declaration | Authored behavior |
|---|---|---|
| Python | Python() | One standard Python visit per incoming edge. |
| Agent | Logical profile and session IDs | One agent-aware visit per incoming edge. |
| Feature | Transparent feature-state operation | One feature visit per incoming edge; no node impl.py or local state. |
| Subroutine call | Target subroutine ID and parameter closure | A call visit invokes the child in the current process. |
| Workflow call | Target workflow ID and project path | A call visit invokes the child in its own workflow process. |
Generated interface
The generated ◆ nodes/<node>/__init__.py exports NODE, which binds the node's identity to its operation. For a non-Feature node, it also imports the authored Output and State types and supplies State as state_type. Python, Agent, and call nodes use NodeDefinition; Feature nodes use FeatureNodeDefinition without an output or local-state type. A control port exports PORT, a PortDefinition containing its typed node ID. Each incoming edge has a separate visit interface that combines the source payload type with these target types.
The node declaration selects the operation and resources. The visit's visit_impl supplies the behavior when that edge enters the node.
Authored files
Node types
For every non-Feature executable node, ● nodes/<node>/impl.py defines:
| Type | Responsibility |
|---|---|
Output | The payload returned by any successful visit to this node and passed to the next step. |
State | Values needed by later visits to this node within the same subroutine invocation. |
The runtime constructs State() at invocation start and requires a frozen, slotted dataclass with a generated initializer and direct instance fields. Fields have immutable direct defaults; inheritance, ClassVar, InitVar, default_factory, and other field customization are unsupported. Values needed by another node belong in Output. Feature nodes and control ports have no node-level authored file. Per-edge behavior belongs to the visit.
Agent nodes
An Agent node selects its profile and session independently. The profile specifies the provider and its options; the session controls conversation continuity. Within a resource scope, one profile can serve multiple independent sessions, while Agent nodes selecting the same persistent session share its conversation.
Call boundaries
The generated SubroutineCall or WorkflowCall operation records the child's definition_id, definition_module, and project_path. A subroutine call also imports ChildInput, ChildOutput, ChildParams, and every parameter type reachable through further subroutine calls. Its params_types records that parameter closure; profile_arguments and session_arguments bind child parameter IDs to caller resource IDs.
Local workflow calls expose ChildInput and ChildOutput. External workflow calls use object for both types because the caller does not import the child's process-boundary classes. External subroutine calls expose the resolved child types through imports from the pinned project's package.
A successful call visit must invoke context.invoke() exactly once. It adapts the child input and returned value to the parent node's Output; an optional params= argument overrides the child parameters for that invocation. This is a synchronous durable effect inside a deterministic adapter. See Call visits for its replay and side-effect requirements.
Feature nodes
A Feature node has no node-local State or authored Output. Generated visits downstream infer the payload type from its upstream producers. A Feature visit returns only candidate feature state; the runtime retains the original payload.
A Feature node is distinct from a feature declaration, which names one component of subroutine state and specifies its domain. A Feature visit assigns its first value as well as subsequent values.
Control ports
Every subroutine has enter, exit, and failure ports. Ports have no impl.py, local state, or visit implementation. Enter introduces the subroutine Input; exit accepts its successful Output; failure records exceptional termination but executes no handler.
Files
Each executable node has a generated declaration and, except for Feature nodes, an authored module containing its output and state types. The enclosing subroutine maintains one generated node registry.
General node structure
nodes/
├── ◆ __init__.py
├── <control-port>/
│ └── ◆ __init__.py
└── <executable-node>/
├── ◆ __init__.py
├── ● impl.py non-Feature nodes only
└── visit/<incoming-edge>/
├── ◆ __init__.py
└── ● impl.py
| File | Role |
|---|---|
◆ nodes/<node>/__init__.py | Declares NODE, including the operation and, for non-Feature nodes, the authored output and state types. |
◆ nodes/<control-port>/__init__.py | Declares the control port's PORT identifier. |
◆ nodes/<node>/visit/<edge-id>/__init__.py | Resolves that incoming edge's exact input type and declares the node-kind-specific visit adapter. |
◆ nodes/__init__.py | Imports every executable NODE in the subroutine and exports the NODES tuple. Control ports are assembled separately. |
● nodes/<node>/impl.py | Declares the successful Output and private State for every non-Feature executable node. |
● nodes/<node>/visit/<edge-id>/impl.py | Implements the behavior selected when that edge enters this node. |
Countdown example
The Countdown graph contains four executable nodes:
| Node | Kind | Role |
|---|---|---|
initialize_counter | Feature | Proposes the input count as the initial counter. |
check_counter | Python | Forwards the current Count; outgoing conditions select the next step. |
decrement | Agent | Proposes the next Count using the decrementer profile and countdown session. |
update_counter | Feature | Validates the proposal and updates counter, forwarding the payload unchanged. |
The graph also contains the required enter, exit, and failure ports. failure has no incoming edge in this example; an exception still terminates the graph through its failure boundary.
Decrement node
Source: project.json
json
{
"id": "decrement",
"name": "Decrement",
"kind": "agent",
"operation": {
"profile": "decrementer",
"session": "countdown"
}
}The decrement entry declares an Agent node and selects the decrementer profile parameter and countdown session. Generation resolves these names to the typed identifiers in the following module.
Source: ◆ subroutines/main/nodes/decrement/__init__.py
python
from typing import Final
from verdog_runtime.declarations import NodeDefinition, Agent
from demo.countdown.subroutines.main import (
NODE_IDS,
StateScope,
PROFILE_IDS,
SESSION_IDS,
)
from .impl import Output as Output, State
NODE: Final[NodeDefinition[State, StateScope]] = NodeDefinition(
id=NODE_IDS.decrement,
name="Decrement",
state_type=State,
operation=Agent(
profile=PROFILE_IDS.decrementer,
session=SESSION_IDS.countdown,
),
)NODE binds the node's identity to its Agent operation. state_type=State specifies the local state that its visits receive. Output and State are imported from the authored sibling module.
Source: ● subroutines/main/nodes/decrement/impl.py
python
"""The agent returns a count and retains its number of proposals."""
from dataclasses import dataclass
from demo.countdown.subroutines.main.impl import Count
@dataclass(frozen=True, slots=True, kw_only=True)
class State:
attempts: int = 0
Output = CountOutput = Count makes the proposed count the payload passed to update_counter. The number of proposals stays in decrement's State, because only revisits to this node need it. The Agent visit implements the provider call and returns both values in Success.
Feature node
Source: ◆ subroutines/main/nodes/update_counter/__init__.py
python
from typing import Final
from verdog_runtime.declarations import Feature, FeatureNodeDefinition
from demo.countdown.subroutines.main import NODE_IDS
NODE: Final[FeatureNodeDefinition] = FeatureNodeDefinition(
id=NODE_IDS.update_counter,
name="Update counter",
operation=Feature(),
)update_counter uses FeatureNodeDefinition, so it declares neither Output nor a node-local State. Its update visit validates the incoming proposal and returns candidate feature state; the runtime forwards the original payload.
Enter port
Source: ◆ subroutines/main/nodes/enter/__init__.py
python
from typing import Final
from verdog_runtime.declarations import PortDefinition
from demo.countdown.subroutines.main import NODE_IDS
PORT: Final[PortDefinition] = PortDefinition(id=NODE_IDS.enter)The declaration gives the enter port its typed identity. It has no operation or visit implementation. The subroutine's graph factory imports this PORT as ENTER and uses it as the starting point; exit and failure ports follow the same declaration shape.