Skip to main content

Flow source visualization

dexcli visualize turns one Go, Python, or TypeScript Flow source file into a static Flow Definition Graph. The graph shows every possible path that can be determined from the source. It does not represent one Flow run or its history.

dexcli visualize ./order_flow.go

The default schema version is 1.0. Go, Python, and TypeScript sources can opt into Version 2.0 for Dex Web v2:

dexcli visualize ./refund_flow.go --schema-version 2.0

This opens the local Flow Rendering page and keeps it available until you press Ctrl+C. Use --json when you need a Flow Definition Graph artifact. Without --out, it writes JSON to stdout. Use --out to choose a file prefix:

dexcli visualize ./order_flow.py --json --out ./build/order-flow

Use --json to write the JSON to stdout:

dexcli visualize ./order_flow.py --json

Render definitions in Dex Web​

Start Dex with a directory containing generated JSON files:

dexcli dev --flow-rendering-dir ./build

Open Flow Rendering at /v1/rendering. This page is independent from Flow search and run details. It renders the selected definition interactively. Its legend can show or hide control flow, WaitFor, RPCs, Attributes, Channels, Streams, SubFlows, and diagnostics. Flow timeout handlers are always shown as part of the Flow. Streams are hidden by default. The other layers start visible. A Version 2.0 file is drawn with the v2 canvas instead, followed by its diagnostics. See Dex Web.

When Dex starts with a JSON directory, v2 at /v2 also renders the selected Flow type's definition beside the run list. See Dex Web.

Dex scans JSON files recursively and takes a snapshot when it starts. Restart Dex after changing the files. Invalid JSON, an unsupported schema version, or a path that is not a directory stops startup with an error.

What the graph contains​

The main Flow appears as a lavender frame. Each Step is a blue frame inside it. WaitFor paths appear above Execute decisions. A conditional WaitFor or Execute return first passes through a diamond, then branches into separate cards labeled with the source condition.

The definition graph includes:

  • the Flow and start Step
  • registered Steps and statically known transitions grouped by Execute decision
  • condition labels and fixed or runtime-sized fan-out
  • ordered WaitFor conditions, including Channel counts, Timer durations, and external SubFlows
  • RPC and timeout decisions and their next Steps
  • Attribute names, source-language value types, and map status
  • Channel, Attribute, and optional Stream access
  • completion, failure, Channel-empty checks, and cancellation inside Execute cards
  • Execute failure recovery paths

Terminal decisions do not create separate terminal nodes. Cancellation is shown inside the Execute card instead of adding duplicate arrows. The start Step carries a START marker instead of receiving an artificial start edge. Timers appear as rows in WaitFor rather than separate nodes.

An error return or raised exception does not identify another Step. It therefore does not create a transition. An explicit ProceedToOnExecuteFailure or on_execute_failure_proceed_to policy creates a solid magenta recovery edge to the recovery Step. The label records when the recovery skips WaitFor.

Resource arrows have a consistent direction:

  • publisher → Channel → consuming WaitFor
  • Attribute writer → Attribute box → Attribute reader
  • WaitFor → folded SubFlow

Resource and SubFlow relations are dashed, so they remain visually weaker than solid Step transitions. Channels are grouped at the upper left of the Flow. Attributes share one box. RPC hexagons cross the left Flow boundary. A SubFlow is a small dashed frame containing only its Flow type; its internal Steps are not expanded.

The renderer fits the complete Flow into the viewport by default. Channel and Attribute arrows stay hidden until you select that resource or a related Step, WaitFor, Execute decision, RPC, or timeout handler. This keeps the default graph focused on control flow while preserving resource access on demand.

Attribute relations include reads, writes, and locks. The Python analyzer follows static resource aliases on the Flow class, and records Attribute locks declared by RPC or Step options. The Go analyzer attributes a read or write made by a same-package helper to the handler that calls it. The edge records the helper in its metadata.

Stream relations describe observable application output, not control flow. They are hidden by default in Flow Rendering and can be enabled from the legend. The JSON marks Step Stream writes as repeatable and best effort. Heartbeat calls and checkpoint reads are runtime reliability details, so they are not included in the definition graph.

Go reports step_progress_outside_step when an RPC or Flow timeout handler calls Stream.Write or creates a writer with NewBufferedTextStream. The check also follows same-package helpers and buffered writer aliases. The error names the handler, Stream, and helper location. Move the output to a Step. An RPC can publish a Channel message or return NextSteps to schedule that Step.

Supported source profile​

Version 1 supports one Flow per source file. The file must directly contain:

  • static Step registration
  • each registered Step handler
  • Dex transitions and WaitFor definitions
  • RPC next Steps
  • execute-failure recovery targets
  • persistence resource definitions
  • Step Stream progress operations

Ordinary business helpers and imported models are allowed. A helper must not hide a Dex transition, WaitFor definition, resource definition, or recovery target. Resource names must be statically known. Flow types and Step types follow the rules in Flow and Step names.

Go resource access may also be in functions and methods of the same package that a handler calls. The analyzer follows those calls up to six levels deep, including calls inside loops and function literals. A resource passed as an argument is bound to the matching parameter, so access through that parameter counts for the resource.

Go analysis requires a local Go toolchain. The input must belong to a module and package that can be type checked. Python analysis requires Python 3.11 or newer. The Python analyzer uses the standard-library AST. It runs the interpreter in isolated mode with automatic site initialization disabled, so .pth import lines and sitecustomize never run. It never imports or executes the application module.

For Python, the analyzer recognizes Stream outputs yielded by synchronous Step generators and synchronously enqueued Stream writes in asynchronous Step handlers. Step Stream progress in an RPC or Flow timeout handler is invalid and produces a blocking diagnostic.

TypeScript analysis requires Node.js and the typescript package resolvable from the source project, through node_modules or NODE_PATH. The analyzer never loads a compiler from the dexcli working directory. It reads the compiler AST without importing application modules. It recognizes these source shapes:

  • Local Step classes and static Flow fields registered with StepList.startStep, StepList.withoutStartStep, otherSteps, or StepList.empty.
  • Execute decisions returned directly: goTo, goToMany with StepMovement.of, gracefulComplete, forceComplete, forceFail, deadEnd, and forceCompleteIfChannelsEmpty. withCancelingSteps and withCancelingSiblingSteps add cancellations. Named, aliased, and namespace imports of @superdurable/dex all work.
  • Conditions from if, switch, conditional expressions, loops, and try blocks.
  • ExecuteFailure.proceedTo in getStepOptions, directly or through a helper method or function in the same file.
  • Attribute, Channel, and Stream definitions, and manifest-declared Connector factories.

The TypeScript analyzer does not support these features yet:

  • WaitFor methods. A Step with waitFor produces the blocking typescript_wait_for_unsupported diagnostic.
  • More than one Flow in one file. This produces multiple_flows.
  • RPC handlers and handleTimeout. Their decisions are not shown, so a Step that only an RPC schedules can report unreachable_step.
  • Resource reads, writes, locks, state loads, and Stream writes inside local handlers. The graph has no resource edges for local Steps and skips unused-resource warnings.
  • SubFlows, and the Version 2.0 Indexed Attributes, Summary, Display, Actions, and start-input schemas.

An Execute method whose decision is hidden in a helper produces hidden_dex_decision. In TypeScript, every Execute return that is not a recognized decision, such as a helper call or a variable, produces its own hidden_dex_decision, even when another return is recognized. Java, Rust, and recursive multi-file analysis are not included.

Version 1 does not support multiple Flows in one file, reflection, dynamic classes, wildcard imports, getattr-based targets, monkeypatching, or movement collections that escape the handler.

Version 2.0 retains every Version 1 field and requires Step groups and explanations in the same file. Named directive parameters can appear in any order within a line. Multiple directive lines keep source order, which becomes UI order. Unknown, repeated, missing, or invalid parameters are blocking diagnostics. The Go analyzer also reads Indexed Attributes, Summary, Display, Actions, and start-input schemas. See Dex Web for those rules.

Version 2.0 derives an optional start-input schema from the input type of the Step registered with DefineStartStep. It follows JSON field names, ignored fields, pointers, and omitempty. Nested and imported structs, named string and integer constants, time.Time, ordinary numeric widths, slices, fixed arrays, and string-key maps are supported. Integer bounds and integer enum values remain decimal strings in the schema. Recursive types, nesting beyond 32 levels, non-string map keys, interfaces, functions, channels, byte slices, conflicting anonymous fields, JSON string encoding tags, and custom JSON codecs produce a warning and omit the start schema. They do not invalidate the remaining graph.

Version 2.0 recognizes operation-specific Connector factories such as openai.NewCreateResponseStep through the config's canonical Connector SDK marker and generated field metadata, rather than a list of connector or function names. Dex Web shows these Steps with their app's initials. Steps of the llm Connector, which reaches any model provider, read as AI model work. The generic SDK MustNewQueryStep and MustNewMutationStep escape hatches remain supported. Put the factory call directly in DefineStep or DefineStartStep and use an inline config literal. Step type, annotations, named GoTo branch targets (or generic GoToBranch targets), StepRef names, Result Attributes, Streams, and ProceedToOnExecuteFailure targets must be statically visible. Factory annotations supply the Step group and explanation, so the generated Step does not need local directives. Each Connector Step passes only its current operation result to its branch target. Persist business context in an application Attribute before entering the Connector Step. The graph emits one labeled transition per Connector branch, a write relation for the Result Attribute, and best-effort progress relations for structured and text Streams. Dynamic config, branch slice, target, annotation, or resource reference produces a blocking diagnostic. See the Connector factory example.

Python and TypeScript Step annotations​

Put exactly one group directive and one explanation directive directly above each local Step class. Python uses # comment lines; decorators may appear between those comments and the class. TypeScript uses // comment lines directly above the class. Do not separate the comments from the declaration with a blank line. The grammar matches Go:

# dex:group group-id:lookup group-label:"Lookup"
# dex:explanation text:"Look up a catalog record."
// dex:group group-id:lookup group-label:"Lookup"
// dex:explanation text:"Look up a catalog record."

The group ID must be kebab-case. Steps sharing an ID must use the same non-empty label. The explanation is one non-empty sentence describing the Step. FDG 2.0 stores it as metadata.explanation. Missing, duplicate, or malformed directives are blocking errors, including with --lint app.

Connector package manifests​

A Python or npm Connector package ships dex-connector.json. Python places it inside the import package directory. The analyzer resolves imports through the selected interpreter's installed packages and the Flow source directory; --python /path/to/venv/bin/python selects a virtual environment. It reads site-packages directories and the plain directory lines of their .pth files, which covers path-based editable installs. It never runs .pth import lines, so an editable install that relies on an import hook is not found. npm places it next to package.json. TypeScript resolves the imported package through node_modules, starting at the Flow source directory and walking toward the filesystem root. Named import aliases and package namespace aliases are supported. Analysis does not execute the imported Connector.

The manifest JSON schema defines the manifest structure. A manifest has connectorId, an exact releaseTag such as connectors/catalog/v1.2.3, a package object with ecosystem, name, and version, and a non-empty factories array. Package ecosystems are pypi, npm, or go; the Python and TypeScript analyzers require pypi and npm, respectively. Package versions are exact installed versions, independent of the Connector release tag. The CLI also enforces rules that JSON Schema cannot express. Factory names, branch IDs, and ConfigurationUI unit IDs must be unique. Mapped config field names must be distinct. Binding ports must be unique within a unit.

Each exported factory declares name, kind (query, mutation, or trigger), and fields mapping semantic names to source config field names. Query and mutation factories require operationId, stepType and annotations field mappings, and named branches. Every branch has an id, a config field, and optional optional. An unwired optional branch appears as a failure decision. Trigger factories require triggerName and a bindingName field mapping. All factories require a connectionName mapping.

The optional mappings resultAttribute, resultStream, progressStream, and textStream identify declared resources. configurationUI maps a static override; the manifest may also provide a default configurationUI object containing units, their bindings, and JSON pointers. The graph preserves this information for Dex Web. An optional executeFailure mapping names a static recovery Step and adds an Execute failure edge.

Python factory calls use keyword arguments, one inline dictionary, or one inline config constructor with keyword arguments. TypeScript calls use one inline object. Factory annotations is a static object with groupId, groupLabel, and explanation; generated Steps use those values instead of local class comments. Branch fields reference registered local Steps, static Step references, or a direct go_to / goTo wrapper. Trigger bindings must be returned as one static list or array from get_connector_trigger_bindings / getConnectorTriggerBindings.

A Step registration or Trigger binding that calls a name from a package without dex-connector.json produces connector_manifest_missing. The message names the package and the expected manifest path. The same diagnostic reports a package that is not installed. A malformed manifest, or one with a wrong-typed field, produces connector_manifest and names the failing field. Two Steps with one Step type produce duplicate_step_type. That includes two factory calls with one Step type, and a factory Step type that matches a local Step.

Connector nodes emit connectorId, releaseTag, and package inside metadata.connector, together with operation identity, connection name, configurationUI, and configurationEnabled. A non-empty static connection name enables automatic configuration; a dynamic name emits a warning and disables it. Dex Web resolves the exact release tag and verifies its release metadata. Existing Go modulePath / moduleVersion metadata remains supported, and Go graph output is unchanged. This manifest describes analysis metadata; it does not add runtime Connector APIs to an SDK.

Flow and Step names​

The graph uses the Flow type and Step type that the SDK registers. Dex Web v2 uses these names to search runs, match run history to Steps, and start Flows. Go Flows therefore work in Dex Web v2 without a GetFlowType or GetStepType override, unless the analyzer reports one of the diagnostics below.

For Go, the default name is the Go type name without its package or pointer. A Flow type ThreadApprovalFlow and a Step type initializeThread keep those exact names. For Python, the default name is the class name.

Name each Flow type after its domain, such as ApprovalFlow or ReplyFlow. Do not name a Flow type just Flow. The package is not part of the name, so every Flow registered with one Worker needs a different type name. Dex Web, run history, and Flow searches show the name as written.

For Go, a generic Flow or Step type has no default name. Declare a GetFlowType or GetStepType override that returns a non-empty name.

A GetFlowType or GetStepType override can be declared in any file of the package or promoted from an embedded type, including one from another package. Every return statement in the override must return the same compile-time string. An empty string keeps the default name. Dex rejects a Step type that contains /, $, or :. A StepRef target names the registered Step type, not the Go type.

The analyzer reports a blocking diagnostic when it cannot know the registered name or cannot give each Step its own node:

  • dynamic_type_name: an override returns different strings, a value that is not a compile-time string, or a name promoted from an embedded interface.
  • generic_flow_type_name: a generic Flow type has no non-empty override.
  • generic_step_type_name: a generic Step type has no non-empty override.
  • duplicate_step_type: two registrations use one Step type, or one Go type is registered more than once. Step nodes are keyed by Go type, so a generic Step can be registered with only one type argument.

Go node IDs use the Go type, as in step:initializeThread, even when an override changes the Step type.

Python and TypeScript can register several instances of the same Step class when each instance has a distinct static Step type. get_step_type or getStepType can return a string literal, a class attribute initialized with a string literal, or an instance field assigned from a constructor argument whose value is a string literal at registration. Constructor defaults and static variable aliases are also readable. Each node ID uses that instance's Step type, such as step:First and step:Second.

The analyzer reads Step methods only from the registered class itself. A Python Step class must extend only Step and Generic. A TypeScript Step class must not extend another class. Otherwise the analyzer reports step_inheritance_unsupported. Move the shared logic into a helper function and keep each Step method on the registered class.

An instance field is readable only when the class assigns it exactly once. The assignment must be a class attribute, a TypeScript property initializer or parameter property, or a direct statement in the constructor. A field assigned in another method, through setattr, vars, or Object.assign, or from outside the class is not readable. A Python Step class with a decorator, such as dataclass, also has unreadable fields. Construction code that reads a field must run after the field is assigned. A TypeScript property initializer therefore cannot read a field that the constructor assigns. Assign each field in the constructor, or return a string literal from get_step_type or getStepType.

The SDKs resolve an instance target by object identity. A target must be the registered object itself, not another object with the same Step type. Transitions, StepMovement, Execute failure routing, and cancellation accept these targets:

  • The registered instance, read through names that are each bound exactly once. These names include module variables, Flow class attributes or property initializers, Flow fields assigned once in the constructor, Step fields assigned in the constructor, and local variables.
  • A Step class when exactly one instance of that class is registered.
  • A StepRef constructed with a string literal that names a registered Step type. TypeScript must construct it with new.

A constructor call written in a decision creates a new object that is not registered. The SDK rejects that target at runtime, so the analyzer reports unknown_step_target. Target the registered instance or a StepRef instead. Separate Connector factory calls also produce separate nodes when their static Step types differ.

The analyzer accepts an instance only when it can prove which object a name holds. A name bound twice in one function or module is not provable. A reassignment, augmented assignment, loop variable, with target, walrus target, destructuring, global rebinding, or a second declaration in a nested TypeScript block all count as another binding. A constructor parameter that the constructor reassigns is not provable either.

Fields also depend on how the Flow or Step object is used. In Python, use self only to read or assign a field, or pass it to the constructor of a class in the same file. Passing it to another function, storing it in a variable, or rebinding self makes the class fields unreadable and reports dynamic_class. In both languages, a Flow class with a decorator, or a constructor that returns a value, also has unreadable fields.

TypeScript proves fields by checking every use of each tracked object. A tracked object is this inside a class body, an instance that a class in the same file constructs, or a Connector Step that a factory call returns. A tracked object may appear only in these positions:

  • A field read, such as this.target or first.target, including inside callbacks. A getter in the class body may return a tracked object, and reading that getter counts as a field read.
  • A call of a method that its class body declares, such as this.retryOptions().
  • A field assignment through this, such as this.target = target.
  • Registration with StepList.startStep, otherSteps, or withoutStartStep.
  • A Dex target: goTo, StepMovement.of, ExecuteFailure.proceedTo, FlowTimeoutHandlerFailure.proceedTo, the Steps that withCancelingSteps or withCancelingSiblingSteps cancel, or the Execute failure or branch field of a Connector factory config.
  • An alias: a variable, a parameter default, a class field, or a constructor parameter of a class in the same file. Every use of an alias must follow the same rule. An alias may hold several objects.

Any other use is an escape. These uses are escapes:

  • Passing the object to a function or to a constructor of a class outside the file, including Object.assign, Reflect.set, and new Object.
  • Storing it in an array, a Map, a Set, an object literal, or a spread.
  • Returning it, exporting it, or destructuring it.
  • Casting it with as, satisfies, or a type assertion.
  • Reading it with a computed key, or writing through it, such as first.target = value or this[key] = value.
  • Assigning it to a field name that a setter in the class body handles.
  • Using it in any other expression, such as a comparison.

An escape reports dynamic_class at the escaping use. The fields of the escaped object, and of every object it can reach through its fields, become unreadable. An escaped Connector Step reports dynamic_step_target. A computed-key write on any other value, such as words[index] = value, does not affect the graph.

A Flow object created outside its class, such as export const approvalFlow = new ApprovalFlow(), may be exported, returned, or passed to an imported function or constructor, such as new Registry([approvalFlow]). A field read through it, such as approvalFlow.review, is a tracked object. Any other use of the Flow object, such as a cast, a write, a reflective call, or a call of a helper in the same file, is an escape. A class from the same file that is used as a value other than with new, extends, instanceof, a static field read, an export, or a Dex target has unreadable static fields. Tracked objects that its instances store in fields, other than constructor arguments, escape too.

Any access to prototype, proto, globalThis, window, self, or global makes every Flow and Step field unreadable. So does an assignment to an undeclared global name. A reflective write, such as Object.assign, Object.defineProperty, or Reflect.set, whose target is not an object or array literal also makes fields unreadable. A TypeScript getter or setter with a field's name makes that field unreadable.

These checks rely on three assumptions. First, code in other modules does not modify Flow or Step objects. This includes code that receives a Flow object through an export, a return value, or an imported function such as a Registry. Second, the source does not run dynamic code or use reflection that the analyzer cannot see. Third, the source does not change prototypes or global objects in ways that the analyzer cannot see. The analyzer does not follow objects into other modules, so do not pass Flow or Step objects to code outside the source file.

The analyzer reads Flow and Step methods only from the class body. A Python Flow or Step class that defines getattr, getattribute, setattr, delattr, get, set, delete, set_name, new, init_subclass, or class_getitem reports dynamic_class. A Flow or Step method reassigned outside the class body reports dynamic_class in both languages. A decorator on a Flow or Step class or on one of its members reports it too, because the decorator can replace the class or the member. The exceptions are the Dex rpc decorator, Python typing.final and dataclasses.dataclass on a class, and Python staticmethod, classmethod, and typing.override on a method. The built-in Python property is also allowed when it is the method's only decorator. Its name must not be a field or a Step or Flow method that Dex calls. A property setter or deleter still reports dynamic_class. So do Python dict, vars, setattr, and class assignments, and the TypeScript escapes and reflective writes described above.

The analyzer recognizes Dex APIs only through imports of dex or @superdurable/dex. Rebinding a Dex import alias or the dex module name in any scope reports dex_alias_rebinding. Assigning an attribute of a Dex import reports it too. A Python wildcard import from any module reports wildcard_import, because it can rebind Dex names. Python exec, eval, globals, locals, getattr, setattr, delattr, and the builtins module report dynamic_python. So do these names and vars when they are used as values instead of called directly, such as through an alias or as a map argument. TypeScript eval and Function report dynamic_typescript.

Computed Step types, unprovable targets, unknown StepRef names, and ambiguous class targets produce blocking diagnostics. A callback or helper that hides an Execute decision still produces hidden_dex_decision; resolving its instance's Step type does not make the callback body readable.

Start inputs​

Go StartFlow validates its payload against the registered starting Step before sending a request. dexcli visualize reports start_flow_input_type_mismatch when a known payload type does not match that Step's input. This is a blocking error in both schemas, with or without --lint app.

Pass a value of the required input type. When the starting Step needs no input, declare dex.None and pass nil. An empty struct is a different type. A pointer and its value type are also different; nil remains valid for an input type that permits it.

The check covers calls in the Flow file and same-package helpers reached by handlers. The target Flow's GetSteps must directly return a Step slice literal with one DefineStartStep. Target declarations may be in another file or imported package. Dynamically chosen Flows, generic Flow types, interface-valued payloads, and unresolved registrations are not checked.

State loads​

A Step method, RPC, or Flow timeout handler receives ordinary Attributes and Channel sizes. It does not receive AttributeMap values or pending Channel messages unless it selects them. See Selective state loading and Selective RPC state loading. A read of unselected state fails at run time, and a failing Step method is retried until its retry policy ends. For Go sources, dexcli visualize reports these reads as blocking diagnostics in every schema version:

  • attribute_map_read_not_loaded: AttributeMap.Get reads a map that no whole-map or instance load selects for this method.
  • attribute_map_enumeration_not_loaded: AllInstanceKeys or MapSize runs without a whole-map load. Exact-instance loads cannot enumerate, and the SDK panics.
  • attribute_map_instance_not_loaded: Get reads a constant instance key, while every load of that map names other constant keys.
  • channel_messages_not_loaded: PendingMessages or FindPendingMessage reads a Channel or ChannelMap that no load selects.

Writes never need a load. Set, Delete, Publish, Channel message deletion, Size, ChannelMap keys and sizes, WaitFor conditions, and GetConditionResults are not reported.

The analyzer reads loads from the declarations in the Flow's package. For a Step, they are GetStepOptions, every GoTo or MovementOf that moves to the Step with WithStepOptions, and every ProceedToOnExecuteFailure that names it. Execute uses the ExecuteLoad fields and WaitFor uses the WaitForLoad fields. For an RPC, they are the RPCOptions passed to DefineRPC, and the exact instances selected by a call in the package that passes the RPC method value with Load or LoadMessages, such as InvokeRPCWithOptions. For HandleTimeout, they are the FlowTimeoutHandlerOptions values in the package.

Options may be literals, package variables, local variables with field assignments, or the results of same-package functions whose parameters come from the call site. A read in a same-package helper is attributed to the handler that calls it. Its diagnostic points at the call in the Flow file and names the helper's file and line.

When another package chooses an RPC's map instance at invocation, declare that on the RPC method:

// dex:invocation-load attribute-map:customer-profiles

Use channel-map to name a ChannelMap. The directive satisfies instance reads of the named map, but not enumeration. A name that is not an AttributeMap or ChannelMap of the Flow produces invocation_load_directive.

The analysis is path-insensitive: a load on one movement into a Step counts for every movement. Calls through interfaces, function values, and reflection are not followed. When load options cannot be evaluated, such as options returned by another package, the default mode reports nothing for that read.

Application lints​

--lint app adds checks for application mistakes that compile and pass the default analysis. The application checks below apply to Go. Python and TypeScript accept the flag and enforce the same FDG 2.0 group and explanation requirements. The state-load diagnostics above are reported in every mode.

dexcli visualize ./order_flow.go --schema-version 2.0 --json --lint app
  • state_load_unresolved: a read needs a load, but the load options of its handler cannot be evaluated. Declare the loads in GetStepOptions, RPCOptions, or a variable of the Flow's package.
  • connector_branches_merged_unread: a Step receives two or more branches of one Connector Step, one of them defect or uncertain. It uses the result but never reads result.Branch or result.Failure, so a failure would be handled as a success.
  • action_state_not_rechecked: an Action RPC never reads the Attribute of its WhenAttributeMatches condition or an Attribute it locks. The condition selects candidate runs; it does not authorize the write. Read the state under the lock and reject a stale or repeated decision.
  • start_flow_request_id_missing: a StartFlow call in the Flow file, or in a helper that a handler calls, has options without RequestID. A retried start cannot be recognized. Derive the RequestID from the business identity and set AlreadyStarted with IgnoreError.
  • cross_flow_read_not_found_unhandled: a Step calls InvokeRPC or InvokeRPCWithOptions without an errors.As check for FlowNotActiveOrNotFoundError or FlowNotFoundError on its call path. A missing Flow then makes the Step retry until its retry policy ends.

Unknown nodes and diagnostics​

Unsupported dynamic control flow is never omitted silently. The output contains an Unknown node and an error diagnostic. The default renderer still shows the partial graph. With --json, a partial JSON artifact is written, and the command exits with status 1.

Warnings do not invalidate the graph. For example, an Attribute whose value type cannot be determined uses unknown and produces a warning. A registered Step that cannot be reached from the start Step or an RPC also produces a warning. Invalid command arguments exit with status 2.

The JSON valid field is false when any blocking diagnostic is present. Consumers should reject editing or deployment based on an invalid graph while still using its known nodes and edges for explanation.

JSON contract​

The Flow Definition Graph v1 schema defines source spans, parent-child relationships, structured WaitFor and decision details, resource types, branch conditions, edge direction, and diagnostics. The Flow Definition Graph v2 schema extends it with ordered groups and Dex Web v2 metadata.

See Dex CLI for commands that operate on running Flows.