Skip to content

Give the parent a fresh turn after a committed child start (child handle capability) #966

Closed

Description

@rmcdaniel

Need

A workflow should be able to start a child, get the committed child identity (child call, child instance, initial run), do other work, and later await the child's result. It should need no unrelated activity, timer, signal, selection winner or visibility query to get another parent turn after the child starts. This is the runtime dependency of sdk-php#185. The SDK contract is proposed in sdk-php#212, design doc. Other SDKs will want the same capability, so treat it as a portable contract, not a PHP-only convention.

Current native behavior (evidence)

Run on 2026-11-02 21:56:53–21:57:02 UTC. The unchanged probe scripts/probe-child-start-contract.php (sdk-php#212) used normally installed PHP SDK 3.6.2 (1c84f255) against ghcr.io/durable-workflow/server@sha256:485295819c7739d3e937a2c89b8593532de0d9c2fbc8f2e7aaddb0b45fe4b319 (native-3.0.1, source 923d45b3), with SQLite, role tokens and disposable containers:

  • The parent's task completion carrying start_child_workflow commits ChildWorkflowScheduled and ChildRunStarted in the same transaction. Both carry child_call_id, the child instance ID and the initial run ID.
  • The completion acknowledgment returns created_task_ids containing only the child's task, and no child identity.
  • All 3 parent polls before child completion were empty. The parent gets a task only after the child resolves.
  • In source, runtime/children.rs::start_child creates a workflow task for the child only. Cluster info advertises child_workflows, child_workflow_runtime_contract, child_workflow_retry_policy and child_workflow_timeouts, and none of them covers this.
  • What's already correct and must be kept: child_call_id is stable through child continue-as-new while workflow_link_id changes; the typed result is preserved; a duplicate completion of the closed claim is refused (recorded=false, reason=already_completed).

This matches the published PHP Server 2.11 baseline recorded in sdk-php#185.

Proposed capability (names are proposals for the Server owner to settle)

  1. Opt-in command mode. start_child_workflow accepts an explicit flag (for example "await": "started") that asks for a parent turn once the start is committed. Without the flag, the current result-waiting behavior and its task cost stay exactly as they are.
  2. Fresh parent turn. In the same transaction that commits the child start, the runtime creates a new parent workflow task with a fresh claim (new task ID and lease). The poll that delivers it includes the committed ChildWorkflowScheduled/ChildRunStarted history. The child may remain open. The original claim stays closed: no further commands on it.
  3. Duplicate safety. Duplicate acknowledgments, redelivery or a retried completion of the original task must not start a second child or create a second parent turn. That is the existing immutable task-receipt behavior, extended to the new task.
  4. Identity is from canonical history only. The initial run ID is frozen from the original scheduling and start. Later continue-as-new or retries do not change what the turn reported. child_call_id is the stable call identity, not workflow_link_id.
  5. Advertise and refuse. Advertise it in cluster info and in the worker-poll/complete server_capabilities, for example child_workflow_start_turn. A runtime without it must refuse the flagged command explicitly (a typed rejection before any child is started), never silently ignore an unknown option.
  6. Policies unchanged. Parent-close policy (including Abandon), cancellation propagation, deadlines and stale-attempt fences behave exactly as for today's child start. Namespace: the child stays in the parent's namespace, and identity never crosses namespaces.

Out of scope here: caller-selected child IDs (workflow#850) and still-open children at parent continue-as-new (workflow#868).

Acceptance

  • The flagged start produces exactly one child plus one fresh parent task while the child is still running; the turn's history carries the committed identity.
  • Duplicate completion or redelivery of the original task produces no second child and no second turn.
  • The unflagged start is unchanged (no extra parent task), shown with a before/after task count.
  • The capability is advertised; a runtime without it refuses the flagged command before any side effect.
  • Child continue-as-new and retries keep child_call_id and the initial run ID in the turn's history; the result resolves through the chain.
  • After release, sdk-php re-runs its probe as a normally installed SDK against the published image.

Linked: sdk-php#185 (feature), sdk-php#212 (design and probe). Both are parked until this lands.

Activity

  1. added
    kind:featureA public product capability or experience is requested
    kind:cross-repositoryWork spans more than one public repository
    priority:untriagedMaintainers have not assigned a priority
    repo:sdk-phpOwned by the PHP SDK repository
    on Nov 2, 2026
  2. rmcdaniel commented on Nov 3, 2026

    @rmcdaniel
    MemberAuthor

    Server owner decision on the wire contract (the issue asks the Server owner to settle the names). Implementation follows once 3.0.2 has shipped. It's a new capability, so it targets the next minor release (3.1.0), not a 3.0.x patch.

    Command

    start_child_workflow gains an optional await field:

    • "result" is the default and today's behavior: the parent's next turn comes when the child resolves, or from other work. Omitting the field means exactly this, with no extra task.
    • "started": in the transaction that commits ChildWorkflowScheduled and ChildRunStarted, the runtime also creates one new parent workflow task, with a fresh task id, lease and claim.
      • The poll delivering it includes both committed events, so identity (child_call_id, child instance id, initial child_workflow_run_id) comes from canonical history only.
      • The child stays open, and its eventual resolution gives the parent its usual later turn.
    • Any other value is refused with 422 (invalid_child_workflow_await) before anything is written, so no child is started.

    Turn semantics

    • At most one start turn per completion. Several await: "started" children in one completion share one fresh parent task.
    • No turn is created if the same completion closes the parent: complete, fail or continue-as-new.
    • The task payload names the wait as workflow_wait_kind: "child_started", with child_call_id, resume_source_kind: "child_workflow_run", resume_source_id: <initial child run>, workflow_sequence and workflow_event_type: "ChildRunStarted". For several children in one completion it names the first; history carries all of them.
    • Duplicates: the completion is under the existing immutable task receipt. A duplicate completion, redelivery or retry of the original task starts no second child and creates no second turn, and the original claim stays closed.
    • Unchanged: parent-close policy (Abandon included), cancellation propagation, deadlines, retries, child continue-as-new (child_call_id stable; the initial run id frozen in ChildRunStarted) and namespace isolation.

    Advertisement and refusal

    • Advertised as child_workflow_start_turn: true in cluster info capabilities and in the worker poll and complete server_capabilities.
    • Older runtimes: native 3.0.x and PHP 2.x ignore unknown command fields, so an SDK must check the capability and refuse await: "started" itself before submitting the child. That's the negotiation; the server never quietly downgrades a value it understands.
    • Unknown values: a 3.1 runtime refuses any await value it doesn't know.

    Acceptance

    As listed in the issue: one child plus one fresh parent task while the child runs; no second child or turn on duplicates; the unflagged start shows the same task count before and after; advertised; continue-as-new and retry identity kept. Tests run on SQLite, PostgreSQL and MySQL, and the sdk-php probe reruns against the published image.

    Workflow Core (embedded) would need the same field and capability for portability; that's the workflow agent's call.

  3. rmcdaniel commented on Nov 3, 2026

    @rmcdaniel
    MemberAuthor

    Implementation in draft #978 (for 3.1.0). One correction to the contract comment above: an unknown await value is refused through the existing command validation, as 422 validation_failed with the message await must be result or started. on commands.N.await, not a separate invalid_child_workflow_await reason. await on another command type gets the field-scope message. Also added: a cancellation-scope checkpoint refuses await: "started" (child_start_turn_not_at_checkpoint), because it keeps the parent's claim. The contract is now in docs/contracts/child-start-turn.md.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    kind:cross-repositoryWork spans more than one public repositorykind:featureA public product capability or experience is requestedpriority:untriagedMaintainers have not assigned a priorityrepo:sdk-phpOwned by the PHP SDK repository

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions