Skip to content

Visit ​

A visit is the implementation selected by an incoming edge to an executable node. Its input type comes from that edge's source; non-Feature visits share their target node's output and state types. Different arrival paths may therefore accept different input types.

Generated interface ​

Each visit's generated ◆ __init__.py provides the contract imported by its authored ● impl.py:

NameMeaning
InputThe source node's output type, or the subroutine input when arriving from enter. Feature nodes forward their upstream payload types.
OutputThe target node's authored output type, for non-Feature visits.
StateThe target node's local state, or FeatureState[StateScope] for Feature visits.
ContextThe subroutine parameters and facilities available to this node kind.
ResultSuccess[Output, State] for non-Feature visits, or FeatureSuccess[StateScope].

Each incoming edge gets its own Input alias, so it excludes unrelated arrival paths. When the source is a Feature node, the alias follows its upstream payload producers; multiple possible producer types form a union. A non-Feature visit receives only its target node's local state, with no access to feature state or another node's state through this interface.

The generated visit(input, state, context, /) forwards to the authored visit_impl with these types. Local type checking checks that call and its return type. The runtime passes payloads without conversion and does not validate arbitrary payload values against their annotations.

The adapter imports visit_impl inside the function, so the authored module can import its generated aliases without a module-initialization cycle. Python, Agent, and Feature targets export VISIT as a VisitDefinition wrapping this adapter. Call targets use CallVisitDefinition with type arguments Input, Output, State, Params, ChildInput, ChildOutput, and ChildParams, in that order. All use the same synchronous visit_impl entry point; call authors do not provide separate preparation and completion functions. The selecting edge imports VISIT directly; there is no visit registry.

Signatures by target kind ​

TargetContext and resultAuthored responsibility
PythonNodeContext[Params] → Success[Output, State]Synchronously perform standard Python behavior and return the next output and state.
AgentAgentNodeContext[Params] → Success[Output, State]Synchronously invoke the configured agent zero or more times and return typed output and state.
FeatureNodeContext[Params] with FeatureState[StateScope] → FeatureSuccess[StateScope]Synchronously propose feature values; the runtime forwards the payload unchanged.
Subroutine callCallContext[Params, ChildParams, ChildInput, ChildOutput] → Success[Output, State]Synchronously invoke the child exactly once, optionally override its parameters, and adapt its result. Resource mappings come from the call declaration.
Workflow callCallContext[Params, ChildParams, ChildInput, ChildOutput] → Success[Output, State]Synchronously invoke the isolated workflow exactly once and adapt its result. A local target uses object for ChildParams; an external target also uses object for both child payload types.

Authored files ​

Implementation ​

● impl.py defines visit_impl(input: Input, state: State, context: Context, /) -> Result. Import those aliases from the generated sibling module. The context exposes output_dir for this execution of the visit. Return the output and replacement local state in Success, or proposed feature state in FeatureSuccess, according to the generated contract.

Keep values needed only during this call in local variables. Values needed by another node travel in the output payload; values needed only on a later visit to this node belong in its local State.

Agent prompts belong in authored templates or reference resources. A visit with one template places it in the adjacent prompt.md.j2; runtime values are supplied when rendering it. The Countdown example shows this arrangement.

Call visits ​

A call visit's generated interface supplies ChildInput, ChildOutput, and ChildParams. Its Context specializes CallContext[Params, ChildParams, ChildInput, ChildOutput], and its Result is Success[Output, State].

The authored function constructs the child input, passes it to context.invoke(), and adapts the returned value to the parent node's Output. The optional params= argument overrides the child's parameters for that invocation. Countdown contains no call nodes.

context.invoke() is synchronous in a call visit. On its first evaluation, Verdog captures the child request and runs or resumes the child. With checkpointing enabled, that request and the eventual outcome are part of the durable call state. Once a child outcome is available, Verdog evaluates the adapter again and the same invocation returns the recorded output. A recorded child exception is raised at the invocation instead, so normal try/except Exception remains available. Subroutine calls preserve the Python exception type; process-isolated workflow calls report child failures as RemoteWorkflowError. At a resumable boundary, only the child execution and its outcome are durable; both pre- and post-invocation adapter code can run again.

Because the adapter can be evaluated more than once, it is deterministic, replay-safe orchestration code rather than a general side-effect boundary. It must:

  • reach exactly one context.invoke() and reproduce the same child input and parameter override on every evaluation;
  • derive decisions only from its arguments and immutable local values; and
  • avoid unjournaled filesystem or network I/O, process launches, time and randomness reads, and mutable global state.

Do not catch BaseException around context.invoke(). Verdog reserves it for cancellation and its internal unwind control, and rejects an adapter that intercepts that control.

Put such effects in ordinary nodes or in the child graph. Pure construction of the child input and pure transformation of its output are safe. Verdog rejects a replay whose child request differs from the journaled request rather than silently invoking a different child. See Runs and resumption for the checkpoint guarantee.

Feature visits ​

A Feature visit receives FeatureState as its state argument and returns FeatureSuccess, which contains no replacement payload. Its candidate feature changes are accepted only when the selected outgoing edge permits them. Feature visits use state.get(FEATURE) and state.replace(FEATURE, value) with the generated feature declarations. They do not access node-local state.

Each feature starts as None. A Feature visit must initialize it before a later node's conditions read it: conditions observe the state before the source visit, while effects compare that state with the candidate. See Feature initialization.

Files ​

A visit lives below its target node and is named by the edge that selects it:

General visit structure

nodes/<target>/visit/<edge>/
├── ◆ __init__.py
├── ● impl.py
└── ● prompt.md.j2        optional Agent prompt

The edge ID distinguishes visits for parallel edges as well as edges with different sources.

FileRole
◆ __init__.pyResolves the exact Input type, defines the node-kind-specific adapter, and exports VISIT.
● impl.pyImplements this one arrival path. It exists for Python, Agent, Feature, and call targets.
● prompt.md.j2An optional prompt template authored for this visit; Verdog does not create it.

Edges into exit or failure create no visit directory.

Countdown example ​

Five of the six edges in Countdown enter executable nodes and therefore select visits:

Incoming edgeTargetBehavior
enter__initialize_counterinitialize_counterInitialize the feature from Count.value.
initialize_counter__check_countercheck_counterForward the initial count.
check_counter__decrementdecrementInvoke the agent and parse the proposed integer.
decrement__update_counterupdate_counterValidate the proposal and update the feature.
update_counter__check_countercheck_counterForward the accepted count.
check_counter__exitexitNo visit; exit is a control port.

Each implementation lives at nodes/<target>/visit/<incoming-edge>/impl.py below src/demo/countdown/subroutines/main/. The following sections show the generated contract and authored implementation for each arrival path.

Agent proposal ​

Source: ◆ subroutines/main/nodes/decrement/visit/check_counter__decrement/__init__.py

python
from typing import Final
from demo.countdown.subroutines.main.nodes.check_counter import Output as Input
from verdog_runtime.declarations import VisitDefinition, AgentNodeContext, Success
from demo.countdown.subroutines.main import Params
from demo.countdown.subroutines.main.nodes.decrement import Output, State

Context = AgentNodeContext[Params]
Result = Success[Output, State]


def visit(input: Input, state: State, context: Context, /) -> Result:
    from .impl import visit_impl

    return visit_impl(input, state, context)


VISIT: Final[VisitDefinition] = VisitDefinition(implementation=visit)

The check_counter__decrement edge selects this generated VISIT. Input is the Count forwarded by check_counter; Output and State come from the target decrement node. The adapter forwards to visit_impl with these types. Context supplies the configured profile and session through context.invoke().

Source: ● subroutines/main/nodes/decrement/visit/check_counter__decrement/impl.py

python
"""Ask the configured agent for one decrement."""

from pathlib import Path

from jinja2 import StrictUndefined, Template
from verdog_runtime.declarations import AgentAccess, Success

from . import Context, Input, Output, Result, State


def visit_impl(input: Input, state: State, context: Context, /) -> Result:
    template = Template(
        Path(__file__).with_name("prompt.md.j2").read_text(encoding="utf-8"),
        undefined=StrictUndefined,
    )
    reply = context.invoke(
        template.render(value=input.value),
        workspace=context.output_dir,
        access=AgentAccess.READ_ONLY,
    )
    return Success(
        output=Output(value=int(reply.strip())),
        state=State(attempts=state.attempts + 1),
    )

The authored visit renders its prompt, invokes the agent with read-only access to the visit's output directory, and parses the reply as an integer. It returns a Count payload and increments the node-local attempts value. This records the proposal; the update visit checks whether it is correct.

Source: ● subroutines/main/nodes/decrement/visit/check_counter__decrement/prompt.md.j2

text
Return only the integer immediately before {{ value }}.

The template contains the prompt text, while value comes from the visit's input. The workflow's requirements supply Jinja2. With Count(value=2), the rendered prompt asks for the integer before 2. A reply of 1 produces Count(value=1). Template and parsing errors follow standard Python exception handling.

Initialization and validation ​

Initialize counter ​

Source: ◆ subroutines/main/nodes/initialize_counter/visit/enter__initialize_counter/__init__.py

python
from typing import Final
from demo.countdown.subroutines.main import Input
from verdog_runtime.declarations import (
    VisitDefinition,
    FeatureState,
    FeatureSuccess,
    NodeContext,
)
from demo.countdown.subroutines.main import Params
from demo.countdown.subroutines.main import StateScope

State = FeatureState[StateScope]
Context = NodeContext[Params]
Result = FeatureSuccess[StateScope]


def visit(input: Input, state: State, context: Context, /) -> Result:
    from .impl import visit_impl

    return visit_impl(input, state, context)


VISIT: Final[VisitDefinition] = VisitDefinition(implementation=visit)

The initialization visit receives the subroutine's Input, because its incoming edge starts at enter. Its State is a view of the subroutine's features, and Result carries proposed feature state without a replacement payload. Context supplies the subroutine parameters.

Source: ● subroutines/main/nodes/initialize_counter/visit/enter__initialize_counter/impl.py

python
"""Initialize the routing feature from the input count."""

from verdog_runtime.declarations import FeatureSuccess

from demo.countdown.subroutines.main.features.counter import FEATURE as COUNTER
from . import Context, Input, Result, State


def visit_impl(input: Input, state: State, context: Context, /) -> Result:
    return FeatureSuccess(state=state.replace(COUNTER, input.value))

COUNTER is the generated feature declaration. state.replace proposes the input count as its first value; returning FeatureSuccess leaves the Count payload unchanged. The outgoing initialize_counter__check_counter edge permits this initialization with counter?. It has no condition on counter, whose pre-visit value is still None. The next node's conditions observe the committed value.

Update counter ​

Source: ◆ subroutines/main/nodes/update_counter/visit/decrement__update_counter/__init__.py

python
from typing import Final
from demo.countdown.subroutines.main.nodes.decrement import Output as Input
from verdog_runtime.declarations import (
    VisitDefinition,
    FeatureState,
    FeatureSuccess,
    NodeContext,
)
from demo.countdown.subroutines.main import Params
from demo.countdown.subroutines.main import StateScope

State = FeatureState[StateScope]
Context = NodeContext[Params]
Result = FeatureSuccess[StateScope]


def visit(input: Input, state: State, context: Context, /) -> Result:
    from .impl import visit_impl

    return visit_impl(input, state, context)


VISIT: Final[VisitDefinition] = VisitDefinition(implementation=visit)

The update visit receives decrement.Output, the agent's proposed Count. Like initialization, it receives feature state and returns FeatureSuccess. It has no node-local state and does not replace the payload.

Source: ● subroutines/main/nodes/update_counter/visit/decrement__update_counter/impl.py

python
"""Accept only the immediately preceding integer."""

from verdog_runtime.declarations import FeatureSuccess

from demo.countdown.subroutines.main.features.counter import FEATURE as COUNTER
from . import Context, Input, Result, State


def visit_impl(input: Input, state: State, context: Context, /) -> Result:
    current = state.get(COUNTER)
    if current is None:
        raise ValueError("counter is uninitialized")
    if input.value != current - 1:
        raise ValueError("the proposed counter must be exactly one lower")
    return FeatureSuccess(state=state.replace(COUNTER, input.value))

The authored check reads the committed counter and requires the proposed integer to be exactly one lower. With counter=2, Count(value=1) passes; Count(value=0) raises an exception. A successful visit proposes the new feature value. The outgoing return edge independently requires a strict decrease before committing the transition. The authored check enforces the exact answer; the declared effect supplies a progress condition for structural termination analysis.

Forwarding the count ​

After initialization ​

Source: ◆ subroutines/main/nodes/check_counter/visit/initialize_counter__check_counter/__init__.py

python
from typing import Final
from demo.countdown.subroutines.main import Input
from verdog_runtime.declarations import VisitDefinition, NodeContext, Success
from demo.countdown.subroutines.main import Params
from demo.countdown.subroutines.main.nodes.check_counter import Output, State

Context = NodeContext[Params]
Result = Success[Output, State]


def visit(input: Input, state: State, context: Context, /) -> Result:
    from .impl import visit_impl

    return visit_impl(input, state, context)


VISIT: Final[VisitDefinition] = VisitDefinition(implementation=visit)

The edge from initialize_counter selects this visit of check_counter. Although the immediate source is a Feature node, its payload originates at enter, so the generated Input is the subroutine input type. Output and State belong to check_counter, and Result requires both in Success.

Source: ● subroutines/main/nodes/check_counter/visit/initialize_counter__check_counter/impl.py

python
from verdog_runtime.declarations import Success
from . import Context, Input, Result, State


def visit_impl(input: Input, state: State, context: Context, /) -> Result:
    return Success(output=input, state=state)

The visit forwards the initial Count and preserves its empty node-local state. Outgoing edge conditions at check_counter then select decrement when counter > 0, or exit when counter = 0.

After an accepted decrement ​

Source: ◆ subroutines/main/nodes/check_counter/visit/update_counter__check_counter/__init__.py

python
from typing import Final
from demo.countdown.subroutines.main.nodes.decrement import Output as Input
from verdog_runtime.declarations import VisitDefinition, NodeContext, Success
from demo.countdown.subroutines.main import Params
from demo.countdown.subroutines.main.nodes.check_counter import Output, State

Context = NodeContext[Params]
Result = Success[Output, State]


def visit(input: Input, state: State, context: Context, /) -> Result:
    from .impl import visit_impl

    return visit_impl(input, state, context)


VISIT: Final[VisitDefinition] = VisitDefinition(implementation=visit)

The return edge from update_counter selects this separate visit of check_counter. Its generated Input is decrement.Output: the Feature node preserves the agent's payload while updating the feature. The two visits of check_counter share the same target output and local-state types.

Source: ● subroutines/main/nodes/check_counter/visit/update_counter__check_counter/impl.py

python
from verdog_runtime.declarations import Success
from . import Context, Input, Result, State


def visit_impl(input: Input, state: State, context: Context, /) -> Result:
    return Success(output=input, state=state)

This visit also forwards its input and state unchanged. It runs after the update visit and return edge have accepted the proposal. For a successful 2 → 1 decrement, it forwards Count(value=1) while the routing feature is already 1; the next selected edge starts another agent visit.