Skip to main content

Dex Web

Dex Web is the operator UI for Dex. It runs as a separate dex-web process that connects to a FlowService, such as server-zd. For local development, dexcli dev starts server-zd and Dex Web together.

Run dex-web​

dex-web is a standalone binary with an embedded UI. It serves the UI, /api/, /healthz, and /readyz, and it does not start a server.

dex-web --flow-service-address 127.0.0.1:8801

Open http://127.0.0.1:8802. The superdurable/dex-web container image runs the same binary. It binds to 0.0.0.0:8802, runs as UID and GID 65532, and includes the system CA bundle. Each push to the main branch publishes superdurable/dex-web:latest, and each web/vX.Y.Z release publishes superdurable/dex-web:vX.Y.Z. Mount Flow Definition Graph directories and token files read-only, with permissions that let that UID read them.

docker run --rm -p 8802:8802 \
-e DEX_WEB_FLOW_SERVICE_ADDRESS=host.docker.internal:8801 \
superdurable/dex-web:latest

Every setting accepts a flag or an environment variable. A flag overrides its environment variable. Settings do not change after startup, except the contents of the credential file. Other environment variables are ignored, including Kubernetes service-link variables such as DEX_WEB_PORT.

FlagEnvironment variableDefault and meaning
--bind-addressDEX_WEB_BIND_ADDRESS127.0.0.1; the image sets 0.0.0.0. Pass 0.0.0.0 when remote clients must connect.
--portDEX_WEB_HTTP_PORT_NUMBER8802; the HTTP port.
--flow-service-addressDEX_WEB_FLOW_SERVICE_ADDRESS127.0.0.1:8801; the FlowService gRPC address.
--flow-rendering-dirDEX_WEB_FLOW_RENDERING_DIRECTORYEmpty; the Flow Definition Graph JSON directory that enables v2.
--tlsDEX_WEB_TLSfalse; encrypts and verifies the FlowService connection.
--tls-ca-fileDEX_WEB_TLS_CA_FILEEmpty; a PEM file added to the system roots. Requires --tls.
--tls-server-nameDEX_WEB_TLS_SERVER_NAMEEmpty; overrides the name checked against the server certificate. Requires --tls.
--flow-service-credential-fileDEX_WEB_FLOW_SERVICE_CREDENTIAL_FILEEmpty; a file with the bearer token that Dex Web sends to FlowService.

Start Flow always uses pull Workers. Two legacy Worker routing options are deprecated and have no effect: --start-flow-worker-target-pull (DEX_WEB_START_FLOW_WORKER_TARGET_PULL) and --start-flow-worker-target-headless (DEX_WEB_START_FLOW_WORKER_TARGET_HEADLESS). Dex Web still accepts them and prints a startup warning when one is provided.

The credential file holds one bearer token. Surrounding whitespace is ignored. A missing or empty file fails startup. If the file later becomes empty or unreadable, RPCs fail until it is restored. Dex Web checks the file for a new token at most once per second, so an external agent can rotate short-lived tokens through it. The token stays in the Dex Web process and never reaches the browser. Use --tls when you send a credential to a remote FlowService.

dex-web --flow-service-address dex.example.com:443 --tls \
--flow-service-credential-file /var/run/secrets/dex/token

/healthz reports only that the process is alive. /readyz validates the current definition snapshot and then calls FlowService HealthCheck with the configured credential. Use /readyz to gate traffic and /healthz for liveness, so a FlowService outage does not restart Dex Web. On shutdown, Dex Web waits up to ten seconds for open HTTP requests.

Access control​

Dex Web has no end-user authentication. Anyone who can reach its port acts with the configured FlowService credential, and Dex Web can show Flow inputs, outputs, Attributes, and Worker stack traces. Place it behind TLS and an authentication-enforcing reverse proxy or gateway, and authorize access by role. Do not expose Dex Web or FlowService directly to the public Internet. Audit access to production Flow details according to your data-handling policy.

The /api requests​

POST, PUT, PATCH, and DELETE requests under /api/, including Flow search, must send Content-Type: application/json. A missing or different media type returns HTTP 415. Add the header to curl calls that use -d:

curl -H 'Content-Type: application/json' -d '{}' http://127.0.0.1:8802/api/...

Cross-origin browser requests return HTTP 403. Dex Web uses the browser's fetch metadata, so a same-origin request works behind a proxy that terminates TLS or rewrites the Host header. It needs no trusted-origin setting. The proxy must pass the Sec-Fetch-Site and Origin headers through unchanged. For a client that sends no fetch metadata, the Origin host must equal the request Host, so configure the proxy to keep the public Host. Forwarded headers do not override these checks. A client that sends neither Origin nor Sec-Fetch-Site, such as curl, is accepted when it sends JSON.

These checks do not stop DNS rebinding. Restrict accepted host names at the reverse proxy, and keep direct access to the Dex Web port limited to trusted clients. When FlowService fails, the browser sees a generic message and the original error goes to the Dex Web log.

Dex Web has two URL prefixes. v1 is the existing Flows search, run details, and Flow Rendering workspace. v2 is the operator workspace: a run list and filters on the left, and the Flow Definition Graph on the right.

Start Dex without a JSON directory to open v1 at /v1/flows. Start Dex with --flow-rendering-dir to open v2 at /v2. The top-right Version menu switches back to v1.

dexcli visualize ./refund_flow.go --schema-version 2.0 --json --out ./build/refund
dexcli dev --flow-rendering-dir ./build

v2 searches current runs for one Flow type. The canvas uses that Flow type's generated definition, so you do not pick a JSON file again. Historical runs stay on v1.

Each definition uses the Flow type and Step types that your Workers register, as described in Flow and Step names. Go Flows need no GetFlowType or GetStepType override unless the analyzer reports one of the name diagnostics listed there. Generate definitions with the dexcli release that matches your Dex Web, and regenerate them after upgrading.

Version 1 remains the default analyzer schema and supports Go and Python. Version 2 currently supports Go only. Version 1 and Version 2 definition files can coexist. Dex Web rejects malformed Version 2 files and duplicate valid definitions for the same Flow type at startup. For example, two package-main binaries that both define main.Flow resolve to one Flow type, and the error names both files. An analyzer result with valid: false remains available on v1 Flow Rendering but does not appear as a v2 Flow type.

Named directives​

Version 2 directives use named values. Parameter position within one line has no meaning. Quote strings containing spaces as JSON strings, and write multiple values as a JSON array.

// dex:field attribute-key:customer-email value-type:string editable:false description:"Customer email" ui-slot:title
// dex:field attribute-key:case-status value-type:string editable:false description:"Case status" ui-slot:status
// dex:field attribute-key:in-email value-type:string editable:false description:"Customer request" ui-slot:subtitle
// dex:field attribute-key:in-charge-ref value-type:string editable:false description:"Charge reference"
// dex:field attribute-key:evidence-state value-type:string editable:false description:"Evidence state"
// dex:field attribute-key:recommended-action value-type:string editable:false description:"Recommendation" ui-slot:recommendation
// dex:field attribute-key:recommendation-rationale value-type:string editable:false description:"Recommendation rationale" ui-slot:reason
// dex:field attribute-key:guardrail-verdict value-type:string editable:false description:"Guardrail verdict"
// dex:field attribute-key:guardrail-rule value-type:string editable:false description:"Guardrail rule" ui-slot:reason

Example: examples/go/products/customer-refund/agentic/workflow.go

Unknown or repeated parameters, missing required parameters, and invalid JSON are blocking diagnostics. Separate indexed Attribute, field, Action, and input lines keep source order. This order becomes column, detail-field, Action, and form order. There is no separate order property.

Every registered Step has one group declaration and one explanation declaration. The explanation is one sentence that states what the Step does. Group order follows the first member's registration position in GetSteps. Step order within a group also follows GetSteps.

A group should do one thing. Make it either one run of Steps that lead to each other, or one choice that a single Step makes between its Steps, such as the tools an agent picks from. The analyzer warns about any other group with v2_step_group_shape and names the group's separate parts, so you can split it. The warning does not make the definition invalid.

The analyzer judges a group by its main story only. A failure-handling Step is a Step that only failure transitions reach, directly or through other failure-handling Steps. A record of exhausted retries and a repair after a crash are examples. These Steps belong to no group on the canvas, and failure transitions never join a group. Failure-handling Steps do not need a group of their own.

Flow canvas​

The v2 canvas draws one card per Step. The card's icon says who acts at the Step: a person, a message from outside, time, a child Flow, an AI model, an app through a Connector, or the Flow itself.

A group of two or more Steps that forms one run or one choice is a stage. Any other group shows its Steps in its labeled band instead. Collapsed draws each stage as one card. Select the card or its ⊕ to open the stage in place, and select ⊖ on the open stage to close it. Expanded opens every stage. Both buttons also return the view to where the canvas opened.

Each Step is drawn with one line in: the transition from the latest Step that leads to it, preferring a Step in its own group. Every other transition is a ↪ chip on the card it leaves. Select a chip to show where it leads. The Actions that answer a waiting Step, the RPC that starts a Step, and how a Step ends the Flow are also chips on its card.

The canvas draws the Flow's main story first. The main story is every Step that the Flow reaches on purpose, however rarely. It includes business rejections, failed checks, and Steps that an RPC moves the Flow to. Failure handling is hidden in both Collapsed and Expanded. A Step card that a failure transition leaves shows ⚠ and the number of its failure transitions. Stage cards do not show this count, so the collapsed view stays quiet.

Select a card with ⚠ to draw its failure handling beside it. The Steps its failures lead to appear in dashed cards. A failure that goes straight to a main-story Step appears as a dashed ↪ pointer, and ↩ chips lead back to the main story. Nothing else moves. Select anything else to hide the failure handling again.

A Step that catches its own error and then chooses the next Step makes an ordinary decision, so it stays in the main story. To hide that handling, return the error so the Step's failure route runs.

When you select a run, the stage that holds the waiting Step opens, and the Step is ringed and labeled now. Failure handling that the run went through is drawn beside the Step that failed into it.

Start Flow​

The Run workspace shows Start Flow when the selected definition contains a generated start schema. The dialog asks for a Flow ID and the start input. The Flow runs on a pull Worker that is connected to the server. It renders nested objects, arrays, string-key maps, enums, booleans, datetime values, optional fields, and explicit null values. Fixed arrays keep their declared length. Integer values remain decimal text in the browser until serialization, so int64 values keep their exact precision.

The Go analyzer derives the schema from the input type of the Step registered with DefineStartStep. It follows JSON field names, ignored fields, pointers, and omitempty. It supports nested and imported structs, named string and integer constants, time.Time, ordinary numeric widths, slices, fixed arrays, and string-key maps. A recursive or unsupported input produces a warning and omits the start schema without hiding the rest of the definition.

The Server reloads the current definition revision and validates the complete input before calling StartFlow. It rejects unknown object fields, missing required fields, invalid nulls, type, enum, range, datetime, array, and map errors. The Server selects the start Step type and creates the request ID.

The POST /api/v2/start request does not need a Worker target. Dex Web starts the Flow on a pull Worker. workerTargetPull: true is accepted. workerTargetPull: false returns HTTP 400. Dex Web does not check for a registered pull Worker before starting, because registrations can change before dispatch. See Run against server-zd.

Worker routing​

Flow detail shows the Worker target, which is Pull. It also shows Flow type queue, named after its Flow type, and Sticky app Worker ID, the last app Worker recorded as serving the Flow.

Active Steps and Step history show routing for WaitFor and Execute. Serving app Worker ID identifies the Worker that served the method. Queue source shows the queue that served the task: Sticky queue or Flow type queue. A Worker may serve a task from the Flow type queue even when its ID matches the Dispatch sticky app Worker ID. For older records without queue metadata, Dex infers the source from those IDs. A pending dispatch shows — until its queue is known. A failed dispatch may show its queue even if no serving Worker is known.

A dispatch failure distinguishes no available Worker, an unknown Step type or RPC name that exhausted the dispatch wait, and a Worker disconnect. For new Step failure records, an unknown registration appears as Unknown Step type: dispatch timed out. RPC history shows the serving app Worker ID for recorded invocations. Older runs without routing metadata show —.

Search and Summary​

An indexed Attribute declaration binds the Dex Attribute key, physical search index key, index type, application value type, and description. The analyzer compares every value with the Go definition and the SDK's effective index configuration.

The physical key can be a generic namespace slot such as keyword2. The selected Flow type scopes its meaning in this workspace, so the UI behavior does not change when different Flow types reuse that slot.

The v2 list searches one Flow type at a time. Filters include Flow ID, execution status, start and close time, and the declared indexed Attributes. Dex Web compiles these controls into a visibility query. It does not accept a raw query from the browser. Different fields use AND; multiple values for one field use OR.

Each row calls GetDexSummary for the logical Flow ID's current run. The RPC accepts dex.None and returns a map[string]any. Dex Web loads at most eight summaries concurrently, applies a five-second timeout to each call, and isolates a failure to its row. Indexed Attributes appear before Summary fields.

Display and edits​

Selecting a row keeps the Flow ID in the URL, not a run ID. GetDexDisplay, Attribute reads, edits, and Actions omit the run ID, so Dex resolves the current run. Refreshing the same URL always addresses the Flow's current run.

On v2, selecting a Step opens a panel split into Definition and Execution. Definition shows the Step explanation, WaitFor, and Execute branches from the Flow Definition Graph. Long Definition content scrolls so Execution stays visible. When a Step has more than three Execute branches, the branch list starts collapsed. Execution has a dropdown when the Step ran more than once, then Input, Output, and Context for the selected WaitFor or Execute history event in the same structured form as v1 Selected event. The execution payload defaults to Details and can switch to Raw JSON of the hydrated payload. AttributeMap and ChannelMap instances that are decimal numbers sort by numeric value, matching v1. Execute shows the live next Steps from that run's decision, not every FDG exit. Use Load more from previous run to see earlier executions of a Step.

A field can also name the UI slot it fills: title, subtitle, status, recommendation, or reason. A UI slot says what part the field plays, so one template serves every Flow while each Flow maps its own Attributes onto it. UI slots are optional and most fields fill none.

Dex Web reads title and status for each row in the run list, and falls back to the Flow ID and the execution status when a Flow declares neither. The selected-run panel reads all five, in UI slot order, ahead of the remaining fields. title, subtitle, status, and recommendation admit one field each. reason admits several, because a recommendation can rest on more than one.

A row carries indexed Attributes and Summary fields only. A UI slot on an Attribute that appears in neither is read in the panel and not in the row. A UI slot is not an index: it says where a value belongs, never that it is searchable.

Display fields are otherwise ordered by their directive lines. Primitive string, integer, double, boolean, and datetime fields can be editable. JSON, objects, arrays, and AttributeMap values remain read-only. Missing fields must be returned as null. The backend rejects omitted, undeclared, or mistyped values.

Edits are available only while the current run is active. Dex Web writes the Attribute through SetAttributes and includes its declared index configuration when the Attribute is indexed. When the edited Attribute is an Action condition source, Dex Web also sends the complete Action permission mapping. Other edits omit it.

Actions​

An Action is an RPC registered with RPCOptions.Action. DefineAction receives its label, an Attribute condition, and one ActionRequiresPermission option. The visualizer reads this typed Go registration directly. Dex Web re-reads the condition Attribute before invoking the RPC.

An Action without input accepts dex.None and renders as a direct button. An Action with input accepts a named Go struct. User-sourced fields render controls in directive order. Attribute-sourced fields stay hidden and use the selected run's Attribute snapshot.

A user-sourced string field can declare capture:qr-code. Dex Web keeps the text field for scanners, paste, and manual entry, and adds Scan QR code. Opening the scanner requests the camera and loads the QR reader. A successful scan copies the decoded text into the field; it does not submit the Action. The user can review the value before choosing the Action.

Camera capture requires HTTPS or localhost. A denied permission, missing camera, unsupported environment, or host policy leaves manual entry available. Closing the scanner, changing runs, or leaving the page releases its camera tracks. Scanning does not grant permission, bypass the Action condition, or change the RPC protocol.

Each Action requires one permission such as refund.manage. Permission keys begin with a lowercase letter and contain lowercase letters, digits, dots, or hyphens. When a successful Worker invocation writes an Action condition source, the Worker sends the complete mapping with that response. Unrelated writes and query-only RPCs omit it.

The Server overlays the business writes on authoritative Attribute state and evaluates the mapping atomically with those writes. It sorts and deduplicates the permissions of currently available Actions, then adds newly matching permissions to the internal DexWorkQueuePermissions KeywordList Search Attribute. Once added, a permission remains for that Flow execution even when no Action currently matches it. Projection maintenance does not require application Steps to lock every Action source.

The permission history lasts for the Flow execution and remains searchable after completion. A Server upgrade preserves permissions that are still stored but does not reconstruct permissions removed before the upgrade. A new, independent Flow execution starts from its own initial Attributes. A changed Action definition is first evaluated when a later source write sends its mapping or a new Flow starts. Removing or renaming an Action permission does not clear the old value from existing permission history.

Work Queue offers the permissions declared by the selected Flow type. Choosing Working as one permission sends it as workQueuePermissions to POST /api/v2/search. The API also accepts several permissions for callers that derive them from multiple user roles. A run matches when its permission history contains any requested permission. This permission group is joined with the Flow type and other filters using AND.

A matching permission means an Action is or was available; it does not mean the Action is available now. A keyed gate also needs its current key, and the list reads an eventually consistent index. The run decides which Actions are currently available once it is open.

The browser condition only controls presentation. Every Action RPC must check the current state again before changing durable state or publishing a Channel message. Use locks when that check and the effect must be atomic.

Work Queue does not add login or enforce authorization. Working as selects work; it does not grant the chosen permission. The trusted application boundary remains responsible for authenticating users, mapping roles to permissions, and limiting which permissions it sends. Deploy Dex Web behind that boundary.

Run against server-zd​

server-zd does not serve Dex Web. Point --flow-service-address at the server-zd gRPC address. When server-zd authentication is on, pass --flow-service-credential-file with a static token or service JWT. Dex Web also supports TLS for the upstream connection through --tls, --tls-ca-file, and --tls-server-name.

dex-web --bind-address 0.0.0.0 --port 8802 \
--flow-service-address dex-server-zd:8801 \
--flow-service-credential-file /etc/dex/web-token

For local development, dexcli dev starts server-zd and Dex Web. --engine zd remains accepted for existing scripts. The dexcli release package includes dex-server-zd, so a Homebrew install of dexcli needs no separate download. To use another dex-server-zd binary, pass --zd-binary or set DEX_SERVER_ZD_BINARY. Without either, dexcli uses the dex-server-zd next to the dexcli executable, then the one on your PATH. Pass --zd-config with a server-zd YAML file to override settings such as closed Flow retention, Streams, or the search backend. The CLI README lists every flag.

By default, server-zd writes every acknowledged write to its files before it replies, without fsync. A killed process therefore loses no Flows, and the next start resumes from the same state directory. A host crash or power loss can lose recent writes. The dev.durabilityMode: strict override also survives a host crash, but each write waits for fsync and is much slower on macOS. The dev.durabilityMode: relaxed override keeps writes in memory until a graceful stop. After a crash or kill in relaxed mode, server-zd refuses to start until you delete the state directory.

Earlier server-zd releases defaulted to relaxed mode. A state directory that an earlier release left after a crash or kill is therefore refused in the default and relaxed modes. If every earlier run of that directory used dev.durabilityMode: strict, keep that override. server-zd then accepts the directory, because strict mode flushed every write. A directory that an earlier release stopped gracefully works in every mode.

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

Start Flow uses pull Workers, as described in Start Flow.

Run attempts​

Time travel keeps the run ID and creates a new run attempt. Run attempts are numbered from 1.

The run details page shows a Run attempt selector in place of Run chain. The page opens from a v1 run or from Inspect on v2. Choosing an earlier attempt adds ?runAttempt=N to the page URL and shows that attempt's history. Live history stops for an earlier attempt. Live state always belongs to the current attempt. Time travel from that view searches the selected attempt. A TimeTravelFork event links to the previous run attempt.

When the run attempt changes, the page reloads the whole history. Events after the fork point no longer belong to the current attempt.

GET /api/flows/history accepts an optional runAttempt. Zero or absent selects the current attempt. Each event and GET /api/flows/summary report runAttempt. POST /api/flows/time-travel accepts an optional runAttempt and returns the new one.