Skip to content

Settle open activities and timers when a run's own code closes it (Core workflow#1117 parity) #1193

Closed

Description

@rmcdaniel

Native parity for workflow#1117. Core implementation is workflow#1121 (in review); the decision record is General's 2026-11-13 comment on workflow#1117.

Core contract to match

When a run's own code closes it with complete_workflow, fail_workflow or continue_as_new, in the same transaction and before the terminal event:

  • Each open (pending or running) activity gets ActivityCancelled with cancellation_reason: "run_closed", and its attempt and task are cancelled.
  • Each pending timer gets TimerCancelled, and its timer task is cancelled.
  • Exception: an activity scheduled with cancellation_policy: "abandon" (non-local) is left running. The terminal event (WorkflowCompleted, WorkflowFailed or WorkflowContinuedAsNew) lists it under abandoned_activities: [{activity_execution_id, sequence}]. Its later claim, heartbeats and outcome are accepted, but nothing is appended to the closed run, and the outcome reply is recorded: false, reason: "run_closed".
  • Children are untouched: parent-close policy and the continue-as-new hand-over (server#1184).

After the close:

  • A claim of such an activity's task (including a task left by a pre-upgrade close) is refused with run_closed, without history.
  • A late outcome on a completed or failed run is refused with run_closed. A heartbeat answers activity_cancelled or run_closed.
  • Nothing is ever appended after the terminal event. The Run the Server image and bootstrap job as non-root by default #878 rejection-audit diagnostic is unchanged.
  • Replay: ActivityCancelled events with cancellation_reason: "run_closed" never resolve a selection or parallel-group member, and activity outcome lookups skip them, because the workflow never observed them. SDK replayers need the same skip; Core's fiber runner, query replayer and executor do this in Bind Native qualification to the published 3.4.0 index #1121.

Cancel, terminate, timeout and cooperative cancellation already settle open work and are unchanged.

Acceptance

  • Native closes settle open activities and timers with the same events and keys, and keep abandon-policy activities running without history.
  • The SDK replayers (PHP, Python, Rust) skip run_closed cancellations in member resolution. That's a follow-up per SDK.
  • Core reference: fixture selection-loser-settled-at-close and tests V2OpenOperationsAtCloseTest.

Owner: server.

Activity

  1. rmcdaniel commented on Nov 13, 2026

    @rmcdaniel
    MemberAuthor

    For General, acting as SDK owner for PHP, Python and Rust. These are the replayer changes each SDK needs once Server sends #1117 history.

    1. Ignore run-close cancellations in member resolution. An ActivityCancelled whose payload has cancellation_reason: "run_closed" was recorded by the run's own close, after the workflow had moved past that operation. Replay must not treat it as an outcome:
      • it must not resolve a select()/selection member, and must not count as the event that made the winner terminal;
      • it must not be a member failure in an all()/parallel group, whether for failure selection or for 'earliest close time, lowest index';
      • a plain awaited activity never gets one (the workflow awaited it, so it wasn't open at close), so skipping it everywhere is safe.
        In each place the replayer indexes activity outcomes by step sequence, skip these events, and leave the activity as still open.
    2. Accept the new payload keys: abandoned_activities: [{activity_execution_id, sequence}] on WorkflowCompleted, WorkflowFailed and WorkflowContinuedAsNew, and cancellation_reason on ActivityCancelled. A strict payload-key validator must allow them.
    3. Activity workers: a late claim, heartbeat or outcome for an activity of a closed run now gets run_closed (claim refusal, recorded: false with reason run_closed, or heartbeat can_continue: false). Treat it as final and don't retry. For an abandon-policy activity kept running after close, heartbeats keep answering can_continue: true, and the outcome is accepted with recorded: false, reason: run_closed. That is success from the worker's point of view; nothing is recorded on the closed run.

    Core reference: workflow#1121. Core does (1) in ParallelChildGroup::resolutionEventForMember and in the activity outcome lookups of WorkflowExecutor, QueryStateReplayer and WorkflowFiberRunner::indexRecordedActivityOutcomes. The fixture is selection-loser-settled-at-close, and V2OpenOperationsAtCloseTest::testACancellationRecordedAtCloseNeverReplacesTheFailureTheWorkflowHandled is the vector for the all() case.

  2. added
    repo:serverOwned by the standalone server repository
    kind:featureA public product capability or experience is requested
    priority:untriagedMaintainers have not assigned a priority
    status:triageAwaiting maintainer classification
    priority:P1High-priority product or release risk
    status:readyReady for implementation
    and removed
    priority:untriagedMaintainers have not assigned a priority
    status:triageAwaiting maintainer classification
    on Nov 13, 2026
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

    agent:serverkind:featureA public product capability or experience is requestedpriority:P1High-priority product or release riskrepo:serverOwned by the standalone server repositorystatus:readyReady for implementation

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions