microsoft/semantic-kernel · error · InvalidOperationException

The parent process Id must be set before scoping to the pare

Error message

The parent process Id must be set before scoping to the parent process.

What it means

ScopedActorId builds a namespaced actor ID by prefixing with either this process's own ID or its parent's ID. When scopeToParent is true, it requires ParentProcessId to be non-empty. This is thrown as an InvalidOperationException (not KernelException) indicating a programming contract violation: a caller requested parent-scoping on a process that has no parent.

Source

Thrown at dotnet/src/Experimental/Process.Runtime.Dapr/Actors/ProcessActor.cs:504

    private async Task<DaprProcessInfo> ToDaprProcessInfoAsync()
    {
        var processState = new KernelProcessState(this.Name, this._process!.State.Version, this.Id.GetId());
        var stepTasks = this._steps.Select(step => step.ToDaprStepInfoAsync()).ToList();
        var steps = await Task.WhenAll(stepTasks).ConfigureAwait(false);
        return new DaprProcessInfo { InnerStepDotnetType = this._process!.InnerStepDotnetType, Edges = this._process!.Edges, State = processState, Steps = [.. steps] };
    }

    /// <summary>
    /// Scopes the Id of a step within the process to the process.
    /// </summary>
    /// <param name="actorId">The actor Id to scope.</param>
    /// <param name="scopeToParent">Indicates if the Id should be scoped to the parent process.</param>
    /// <returns>A new <see cref="ActorId"/> which is scoped to the process.</returns>
    private ActorId ScopedActorId(ActorId actorId, bool scopeToParent = false)
    {
        if (scopeToParent && string.IsNullOrWhiteSpace(this.ParentProcessId))
        {
            throw new InvalidOperationException("The parent process Id must be set before scoping to the parent process.");
        }

        string id = scopeToParent ? this.ParentProcessId! : this.Id.GetId();
        return new ActorId($"{id}.{actorId.GetId()}");
    }

    /// <summary>
    /// Generates a scoped event for the step.
    /// </summary>
    /// <param name="daprEvent">The event.</param>
    /// <returns>A <see cref="ProcessEvent"/> with the correctly scoped namespace.</returns>
    private ProcessEvent ScopedEvent(ProcessEvent daprEvent)
    {
        Verify.NotNull(daprEvent);
        return daprEvent with { Namespace = $"{this.Name}_{this._process!.State.Id}" };
    }

    #endregion

View on GitHub (pinned to c028a0c7dc)

Solutions

  1. Pass null (not string.Empty) as parentProcessId when the process has no parent, so the SendOutgoingPublicEventsAsync guard correctly skips the parent-scoping path.
  2. Inspect the persisted Dapr actor state for ActorStateKeys.StepParentProcessId and ensure it is either a valid GUID/id or absent, never an empty string.
  3. If initializing programmatically, verify that ParentProcessId is set to a real parent actor ID before the process starts executing.

Example fix

// before
await proxy.InitializeProcessAsync(processInfo, parentProcessId: ""); // empty string

// after
await proxy.InitializeProcessAsync(processInfo, parentProcessId: null); // null when no parent
Defensive patterns

Strategy: validation

Validate before calling

// When initializing a process, pass null (not empty string) when there is no parent:
string? parentProcessId = hasParent ? parentActorId : null;
await processProxy.InitializeProcessAsync(processInfo, parentProcessId);

// Validate before sending:
if (string.IsNullOrWhiteSpace(parentProcessId))
{
    logger.LogDebug("No parent process; skipping parent-scoped operations.");
}

Try / catch

try
{
    await process.StartAsync(keepAlive: true);
}
catch (InvalidOperationException ex) when (ex.Message.Contains("parent process Id must be set"))
{
    logger.LogError("ScopedActorId called with scopeToParent=true but ParentProcessId is not set. Ensure parentProcessId is a valid ID, not empty string.");
}

Prevention

When it happens

Trigger: ScopedActorId is called with scopeToParent=true when ParentProcessId is null or whitespace. This path is only taken from SendOutgoingPublicEventsAsync (line 460), which itself guards with 'if ParentProcessId is not null' — so the throw indicates ParentProcessId was set to a non-null-but-whitespace value, or the guard and the call are inconsistent at runtime.

Common situations: ParentProcessId was assigned an empty string or whitespace during initialization (e.g. parentProcessId argument to InitializeProcessAsync was string.Empty rather than null). Also from a corrupted persisted state where StepParentProcessId was saved as an empty string.

Related errors


AI-assisted analysis of microsoft/semantic-kernel@c028a0c7dc (2026-08-13). Data as JSON: /api/errors/9f73f7b446a8a09a. Report an issue: GitHub.