ifURI examples
← all examples

Docker URI flow

This example demonstrates URI-addressed resources communicating across Docker services:

through the URI target hostname.

Every worker exposes:

The Dockerfiles include io.tellmesh.urirun.manifest=/app/bindings.json, so the image declares where its URI package manifest lives.

The important idea is that the registry is generated from the artifacts that already describe the system. The flow does not hard-code Python, Node.js, shell, or Docker details; it calls URI resources.

Dockerfile labels + bindings.json + scripts + Makefile
  -> make registry
  -> generated/bindings.v2.json
  -> generated/registry.json
  -> flow validation
  -> service dispatch

Registry Generation

Generate a registry from the supplied artifacts:

cd 09-docker_uri_flow
make registry

This runs:

PYTHONPATH=../../../adapters/python python3 -m urirun.v2 scan . \
  --out generated/bindings.v2.json \
  --registry-out generated/registry.json

PYTHONPATH=../../../adapters/python python3 -m urirun.v2 validate generated/bindings.v2.json
PYTHONPATH=../../../adapters/python python3 -m urirun.v2 list generated/registry.json

The scanner discovers:

Generated files are written to generated/:

The orchestrator mounts generated/registry.json and validates that every URI referenced by the flow exists in the generated registry before it calls any service.

generated/ is ignored except for .gitignore; these files are reproducible runtime artifacts, not source files.

Runtime Environment

The example defaults to Docker Compose service DNS and port 8080, but the same flow can run against a different topology:

for example {"python-worker":"http://127.0.0.1:18080"}.

In Compose, URI_SERVICE_MAP is not needed because URI targets such as python-worker resolve directly on the Docker network.

Calling URI Commands

Each worker exposes the same small HTTP surface:

The default Docker Compose file keeps workers inside the Compose network and does not publish their ports to the host. For browser or host-shell calls, run a worker locally on an explicit WORKER_PORT, or add a ports: mapping to docker-compose.yml.

Browser

Start the Python worker in one terminal:

cd 09-docker_uri_flow
WORKER_PORT=18080 python3 python-worker/server.py

If 18080 is already in use, choose another free port and use the same value in the browser URL and shell commands below.

Open the routes endpoint:

http://127.0.0.1:18080/routes

Then open browser DevTools on that page and call the URI command with fetch. Using /run keeps the request same-origin with the opened routes page:

await fetch("/run", {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({
    uri: "python://python-worker/text/normalize",
    payload: { text: " Supplier Report June 2026 " }
  })
}).then((response) => response.json())

The important part is that the browser sends the same URI string that appears in the registry and in the flow. The HTTP endpoint is only the transport adapter.

Shell

The same URI command can be called from shell with curl:

curl -s http://127.0.0.1:18080/routes | python3 -m json.tool

curl -s http://127.0.0.1:18080/run \
  -H 'Content-Type: application/json' \
  -d '{
    "uri": "python://python-worker/text/normalize",
    "payload": { "text": " Supplier Report June 2026 " }
  }' | python3 -m json.tool

For the shell-backed URI, start the shell worker with an output directory:

mkdir -p /tmp/urirun-reports
REPORT_DIR=/tmp/urirun-reports WORKER_PORT=18082 python3 shell-worker/server.py

Then call the shell command through the same /run shape:

curl -s http://127.0.0.1:18082/run \
  -H 'Content-Type: application/json' \
  -d '{
    "uri": "shell://shell-worker/report/write",
    "payload": {
      "slug": "supplier-report-june-2026",
      "text": "supplier report june 2026"
    }
  }' | python3 -m json.tool

To inspect all generated URI routes from shell:

make registry
cat generated/routes.txt

Adding Another URI Package

To add another service or package to this example:

  1. Add a service directory, for example report-worker/.
  2. Add a bindings.json file with v2 bindings owned by that service.
  3. Add this label to its Dockerfile:
LABEL io.tellmesh.urirun.manifest=/app/bindings.json
  1. Add the service to docker-compose.yml.
  2. Run:
make registry

The new URIs should appear in generated/routes.txt. If the flow references a URI that is missing from generated/registry.json, the orchestrator fails before it dispatches any service call.

Flow

The flow format mirrors the compact office examples from uri2flow:

steps:
  - id: normalize_text
    uri: python://python-worker/text/normalize
    payload:
      text: "Supplier Report June 2026"

  - id: slugify_text
    uri: node://node-worker/text/slugify
    depends_on:
      - normalize_text
    payload:
      text_from: normalize_text.result.normalized

Fields ending in _from read values from previous step results.

Run

bash run.sh

Equivalent explicit steps:

cd 09-docker_uri_flow
make registry
docker compose up --build --abort-on-container-exit --exit-code-from orchestrator
docker compose down -v --remove-orphans

make run is the same workflow wrapped by the Makefile:

cd 09-docker_uri_flow
make run

Expected final path:

/data/supplier-report-june-2026.txt

The point is that the orchestrator only sees URI resources and JSON payloads. It does not need to know whether the backing implementation is Python, Node.js, a shell script, a package script, or another Docker image.

Run locally (no Docker)

The workers and orchestrator are portable, so the whole flow also runs without Docker - which is how it is tested in CI:

python3 test_flow_e2e.py   # PASS docker_uri_flow e2e (no docker)

The harness starts each worker on an ephemeral port and points the orchestrator at them. Three environment hooks make this possible (all unset in Compose, so the Docker path is unchanged):

envused bypurpose
WORKER_PORTeach workerlisten port (default 8080)
REPORT_DIRshell workeroutput dir for reports (default /data)
URI_SERVICE_MAPorchestratorJSON {host: base-url} to resolve services outside Docker DNS
URI_SERVICE_MAP='{"python-worker":"http://127.0.0.1:9001","node-worker":"http://127.0.0.1:9002","shell-worker":"http://127.0.0.1:9003"}'

Library-native dispatch (urirun.v2_service)

Workers implement their URI resources natively, so their images stay dependency-free; the registry is the shared contract. A coordinator therefore needs no bespoke HTTP code - urirun.v2_service turns "call this URI on its worker" into one library call that validates the payload against the registry's JSON Schema first, then POSTs to the worker (resolving the host via the same URI_SERVICE_MAP). It is adapter-agnostic - it works whatever the worker labels the route.

from urirun import v2, v2_service

registry = v2.compile_registry(merged_worker_bindings)
env = v2_service.call("python://python-worker/text/normalize", {"text": "Hi"}, registry)
#   env["ok"] / env["result"]["normalized"]   (schema-checked, then dispatched)

See test_service_adapter.py for dry-run, schema-rejection, unknown-URI, and live-call coverage.

Docker test environment

docker-compose.test.yml builds the three workers plus a library-based tester container (tester/) that has urirun installed. On the Compose network the tester:

  1. discovers routes by calling each worker's GET /routes,
  2. validates a bad payload and an unknown URI against the registry schema,
  3. dispatches the whole flow with urirun.v2_service over Docker DNS

(URI_SERVICE_MAP unset -> http://<service>:8080).

Its exit code drives the run, so this is a self-contained integration test of the library against real, networked, polyglot services:

cd 09-docker_uri_flow
make test-docker          # or: bash run_tests.sh
tester-1  | discovered 4 routes from 3 services
tester-1  | flow: supplier report june 2026 -> supplier-report-june-2026 -> /tmp/supplier-report-june-2026.txt
tester-1  | PASS docker_uri_flow library dispatch (in-container)

Files

.gitignoreMakefileREADME.mddocker-compose.test.ymldocker-compose.ymlgenerate_registry.shrun.shrun_tests.shtest_flow_e2e.pytest_flow_runner.pytest_service_adapter.py.benchmarks/flows/node-worker/orchestrator/python-worker/shell-worker/tester/

View on GitHub →