A Workflow Should Join on Evidence, Not on Hope

A fan-out workflow becomes hard to operate at the moment its first worker finishes: what exactly is allowed to happen next? A small DAL challenge makes the answer concrete—launch independent checks, collect their results, and produce one reviewable package only when the join condition is satisfied.

This article walks through a worked orchestration pattern for distributed agents: a bounded scatter, an evidence-bearing join, and a deliberate stop before any external effect.

The challenge: coordinate three independent checks

Imagine a release-preparation workflow with three independent questions:

  • Does the proposed change have the required source and test evidence?
  • Does the documentation describe the behavior that is actually shipping?
  • Does the deployment checklist contain a rollback path?

A naive coordinator asks three agents to “check the release,” waits for messages, and asks another agent to summarize them. That sounds parallel, but the workflow has no durable definition of completion. One worker may return a paragraph instead of evidence. Another may silently fail. The summarizer may mistake silence for approval.

The challenge is to implement this rule:

Create a review package only when every required check has a recorded outcome. If any check fails or is incomplete, preserve the failure and stop without publishing or deploying.

The important design choice is that the join waits for outcomes, not merely responses.

Start with a small result contract

Before writing orchestration, define the result each worker must leave behind. A useful result has an identity, a status, an artifact or citation, and a next action. In DAL-style notation, the contract can be sketched like this:

```dal record CheckResult { check_id: text status: text // pass | fail | blocked evidence: path summary: text next_action: text }

checks = [ "source-and-tests", "documentation", "rollback" ] ```

The exact syntax may vary with the runtime, but the boundary is the point: a worker does not complete by speaking confidently. It completes by producing a result that another agent can inspect.

For a blocked check, evidence can point to a blocker note rather than pretending that a missing input is a pass. This gives the coordinator three honest states instead of the dangerous binary pair “message received” and “message missing.”

Scatter bounded work, not open-ended autonomy

Now fan out the independent checks. A schedule or scatter primitive is useful here because it makes the parallelism explicit while keeping the work set finite:

```dal workflow release_review(change: path) { results = scatter checks as check_id { run check_agent( check_id: check_id, input: change, output: "artifacts/release-review/" + check_id + ".result" ) }

join release_review_results(results) } ```

There are two boundaries worth noticing. First, checks is a fixed list. The workers cannot recursively create more workers merely because they found an interesting question. Second, each worker has a distinct output path. That prevents the join from having to infer which paragraph belongs to which check and makes retries idempotent at the artifact level.

A production implementation should also carry a step or time budget. If the rollback worker cannot access the deployment manifest, it should write blocked, not keep browsing indefinitely. Orchestration is not improved by hiding an unbounded tool loop inside a parallel branch.

Make the join a gate with explicit failure behavior

The join is where many agent workflows become wishful. “All agents returned” is not a sufficient condition. The coordinator should load each expected result, verify its shape, and evaluate the statuses:

```dal join release_review_results(results) { required = ["source-and-tests", "documentation", "rollback"] complete = results.have_one_result_for_each(required)

if not complete { write_blocker( path: "artifacts/release-review/join.blocked.md", reason: "A required check has no verifiable result" ) stop }

if results.any(status == "fail" or status == "blocked") { write_review_package( path: "artifacts/release-review/review.md", results: results, decision: "changes_required" ) stop }

write_review_package( path: "artifacts/release-review/review.md", results: results, decision: "ready_for_review" ) stop } ```

This is deliberately boring. A missing result is not converted into a pass. A failed check is not erased because two other workers succeeded. The workflow produces a package for human review, then stops before publication or deployment.

That last transition matters. The coordinator may prepare a proposed external action, but it should not smuggle approval into the join. “Ready for review” is a durable state; “deployed” is a separate, authorized action with its own evidence.

Preserve partial progress without confusing it for completion

Suppose the documentation and rollback workers pass, but the source-and-tests worker is blocked. Throwing away the two successful results guarantees duplicate work on the next run. Marking the whole workflow complete is worse.

Keep the partial result set under the workflow’s artifact directory:

``text artifacts/release-review/ source-and-tests.result # blocked documentation.result # pass rollback.result # pass join.blocked.md ``

The next run can inspect existing results, retry only the blocked check when its dependency is available, and rerun the join. This is a recovery pattern, not just a storage detail. The queue or scheduler should carry the next bounded action—“supply test report, then retry source-and-tests”—rather than the vague instruction “continue release review.”

A useful checkpoint also records what did not happen: no publication, no deployment, and no approval was inferred. Negative evidence prevents a later operator from reading a green-looking partial directory as a completed release.

Test the orchestration with adversarial cases

Before trusting the pattern, test the join against the cases that optimistic demos avoid:

  1. One worker returns no file at all. The join must create a blocker.
  2. One worker returns malformed data. The join must reject it rather than summarize it.
  3. One worker reports blocked. The review package must say changes_required or equivalent, never ready.
  4. A worker is retried. Its output path must remain deterministic, and the join must not duplicate the check.
  5. All checks pass. The result must be a review package, not an automatic external action.

These tests turn orchestration policy into something inspectable. They also expose a common anti-pattern: using the language of parallelism while keeping completion logic implicit in a coordinator’s prompt.

Conclusion: join on durable facts

The worked challenge is small, but the lesson scales. Fan-out gives a workflow speed; it does not give it truth. Truth appears at the join, where every required branch must have a typed-enough outcome, durable evidence, and an explicit failure path.

To apply this pattern in DAL, start with three steps:

  1. Define the worker result before defining the worker prompt.
  2. Scatter over a bounded list with deterministic artifact paths.
  3. Make the join distinguish missing, blocked, failed, and passing results—and stop before external effects.

A good orchestrator does not merely know which agents are busy. It knows which commitments have evidence, which need intervention, and exactly why the workflow is allowed to move forward.