Repository navigation
Description
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_workflowcommitsChildWorkflowScheduledandChildRunStartedin the same transaction. Both carrychild_call_id, the child instance ID and the initial run ID. - The completion acknowledgment returns
created_task_idscontaining 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_childcreates a workflow task for the child only. Cluster info advertiseschild_workflows,child_workflow_runtime_contract,child_workflow_retry_policyandchild_workflow_timeouts, and none of them covers this. - What's already correct and must be kept:
child_call_idis stable through child continue-as-new whileworkflow_link_idchanges; 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)
- Opt-in command mode.
start_child_workflowaccepts 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. - 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/ChildRunStartedhistory. The child may remain open. The original claim stays closed: no further commands on it. - 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.
- 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_idis the stable call identity, notworkflow_link_id. - Advertise and refuse. Advertise it in cluster info and in the worker-poll/complete
server_capabilities, for examplechild_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. - 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_idand 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
rmcdaniel commentedon Nov 3, 2026
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_workflowgains an optionalawaitfield:"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 commitsChildWorkflowScheduledandChildRunStarted, the runtime also creates one new parent workflow task, with a fresh task id, lease and claim.child_call_id, child instance id, initialchild_workflow_run_id) comes from canonical history only.invalid_child_workflow_await) before anything is written, so no child is started.Turn semantics
await: "started"children in one completion share one fresh parent task.workflow_wait_kind: "child_started", withchild_call_id,resume_source_kind: "child_workflow_run",resume_source_id: <initial child run>,workflow_sequenceandworkflow_event_type: "ChildRunStarted". For several children in one completion it names the first; history carries all of them.child_call_idstable; the initial run id frozen inChildRunStarted) and namespace isolation.Advertisement and refusal
child_workflow_start_turn: truein cluster infocapabilitiesand in the worker poll and completeserver_capabilities.await: "started"itself before submitting the child. That's the negotiation; the server never quietly downgrades a value it understands.awaitvalue 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.
rmcdaniel commentedon Nov 3, 2026
Implementation in draft #978 (for 3.1.0). One correction to the contract comment above: an unknown
awaitvalue is refused through the existing command validation, as 422validation_failedwith the messageawait must be result or started.oncommands.N.await, not a separateinvalid_child_workflow_awaitreason.awaiton another command type gets the field-scope message. Also added: a cancellation-scope checkpoint refusesawait: "started"(child_start_turn_not_at_checkpoint), because it keeps the parent's claim. The contract is now indocs/contracts/child-start-turn.md.