Skip to content

call-workflow is not wired into the consolidated safe_outputs handler-manager path #21205

Description

@johnwilliams-12

Summary

On gh-aw v0.58.3, a workflow that uses consolidated safe_outputs plus call-workflow can still fail in the Process Safe Outputs step with No handler loaded for type 'call_workflow'. Current main already contains the adjacent HTTP registration fix from #21074 / #21124, but the consolidated safe-output handler-manager path is still missing call_workflow wiring in both the compiler and runtime. This means the feature can compile worker fan-out jobs and MCP tools, but the runtime step that consumes agent messages still skips the selected call_workflow message instead of invoking actions/setup/js/call_workflow.cjs.

Root Cause

This is not a duplicate of #21074. That issue fixed HTTP MCP tool registration for generated _call_workflow_name tools. The remaining bug is later in the execution pipeline.

There are three missing integration points on current main:

  1. actions/setup/js/safe_output_handler_manager.cjs

    • HANDLER_MAP includes dispatch_workflow but does not include call_workflow.
    • loadHandlers() only iterates HANDLER_MAP, so it never requires ./call_workflow.cjs even when config.call_workflow is present.
    • processMessages() then reaches the fallback branch and records:
      • No handler loaded for type 'call_workflow'
  2. pkg/workflow/compiler_safe_outputs_config.go

    • addHandlerManagerConfigEnvVar() builds GH_AW_SAFE_OUTPUTS_HANDLER_CONFIG by iterating handlerRegistry.
    • handlerRegistry has a builder for dispatch_workflow but no builder for call_workflow.
    • Result: compiled workflows do not emit call_workflow into GH_AW_SAFE_OUTPUTS_HANDLER_CONFIG, so even if the runtime map were fixed, the handler would still not be enabled from compiled config.
  3. pkg/workflow/compiler_safe_outputs_job.go

    • hasHandlerManagerTypes includes DispatchWorkflow but does not include CallWorkflow.
    • Result: a workflow that only routes via call-workflow can compile without any Process Safe Outputs step at all, despite call_workflow.cjs being the runtime bridge that sets call_workflow_name and call_workflow_payload.

Relevant current-source evidence:

// actions/setup/js/safe_output_handler_manager.cjs
const HANDLER_MAP = {
	create_issue: "./create_issue.cjs",
	...
	dispatch_workflow: "./dispatch_workflow.cjs",
	create_missing_tool_issue: "./create_missing_tool_issue.cjs",
	missing_tool: "./missing_tool.cjs",
	...
};
// actions/setup/js/safe_output_handler_manager.cjs
if (!handler) {
	core.warning(
		`⚠️ No handler loaded for message type '${messageType}' (message ${i + 1}/${messages.length}). The message will be skipped...`
	);
	results.push({
		type: messageType,
		messageIndex: i,
		success: false,
		error: `No handler loaded for type '${messageType}'`,
	});
	continue;
}
// pkg/workflow/compiler_safe_outputs_job.go
hasHandlerManagerTypes :=
	data.SafeOutputs.CreateIssues != nil ||
	...
	data.SafeOutputs.DispatchWorkflow != nil ||
	data.SafeOutputs.CreateCodeScanningAlerts != nil ||
	data.SafeOutputs.AutofixCodeScanningAlert != nil ||
	data.SafeOutputs.MissingTool != nil ||
	data.SafeOutputs.MissingData != nil
// pkg/workflow/compiler_safe_outputs_config.go
var handlerRegistry = map[string]handlerBuilder{
	"create_issue": ...,
	...
	"dispatch_workflow": func(cfg *SafeOutputsConfig) map[string]any {
		...
	},
	// missing: "call_workflow"
}

For contrast, the already-fixed adjacent path on current main now does recognize _call_workflow_name in actions/setup/js/safe_outputs_mcp_server_http.cjs, so tool registration is no longer the blocker:

const isCallWorkflowTool = tool._call_workflow_name && typeof tool._call_workflow_name === "string" && tool._call_workflow_name.length > 0;

...

} else if (isCallWorkflowTool) {
	logger.debug(`Found call_workflow tool: ${tool.name} (_call_workflow_name: ${tool._call_workflow_name})`);
	if (!safeOutputsConfig.call_workflow) {
		...
		continue;
	}
	logger.debug(`  call_workflow config exists, registering tool`);
}

Affected Code

Broken compiled/runtime behavior appears in two ways.

1. Compiled handler-manager config omits call_workflow

Current compiler logic can emit a safe_outputs job and even generate conditional fan-out jobs, but the handler-manager config is missing the type that runtime needs to load:

# compiled safe_outputs job excerpt
- name: Process Safe Outputs
	id: process_safe_outputs
	env:
		GH_AW_SAFE_OUTPUTS_HANDLER_CONFIG: '{"add_comment":{"max":1}}'
		# BUG: call_workflow is omitted even when safe-outputs.call-workflow is configured

Expected shape:

- name: Process Safe Outputs
	id: process_safe_outputs
	env:
		GH_AW_SAFE_OUTPUTS_HANDLER_CONFIG: '{"add_comment":{"max":1},"call_workflow":{"max":1,"workflows":["worker-a"],"workflow_files":{"worker-a":"./.github/workflows/worker-a.lock.yml"}}}'

2. Runtime handler-manager skips the message even though call_workflow.cjs exists

actions/setup/js/call_workflow.cjs is already the correct runtime implementation for consolidated fan-out selection:

// actions/setup/js/call_workflow.cjs
core.setOutput("call_workflow_name", workflowName);
core.setOutput("call_workflow_payload", payloadJson);

return {
	success: true,
	workflow_name: workflowName,
	payload: payloadJson,
};

But safe_output_handler_manager.cjs never loads it, so the message falls through to the no-handler path instead of invoking this module.

Proposed Fix

Wire call_workflow into the same consolidated path that already handles dispatch_workflow.

1. Add call_workflow to the handler-manager runtime map

Replace the relevant section in actions/setup/js/safe_output_handler_manager.cjs with:

const HANDLER_MAP = {
	create_issue: "./create_issue.cjs",
	add_comment: "./add_comment.cjs",
	create_discussion: "./create_discussion.cjs",
	close_issue: "./close_issue.cjs",
	close_discussion: "./close_discussion.cjs",
	add_labels: "./add_labels.cjs",
	remove_labels: "./remove_labels.cjs",
	update_issue: "./update_issue.cjs",
	update_discussion: "./update_discussion.cjs",
	link_sub_issue: "./link_sub_issue.cjs",
	update_release: "./update_release.cjs",
	create_pull_request_review_comment: "./create_pr_review_comment.cjs",
	submit_pull_request_review: "./submit_pr_review.cjs",
	reply_to_pull_request_review_comment: "./reply_to_pr_review_comment.cjs",
	resolve_pull_request_review_thread: "./resolve_pr_review_thread.cjs",
	create_pull_request: "./create_pull_request.cjs",
	push_to_pull_request_branch: "./push_to_pull_request_branch.cjs",
	update_pull_request: "./update_pull_request.cjs",
	close_pull_request: "./close_pull_request.cjs",
	mark_pull_request_as_ready_for_review: "./mark_pull_request_as_ready_for_review.cjs",
	hide_comment: "./hide_comment.cjs",
	set_issue_type: "./set_issue_type.cjs",
	add_reviewer: "./add_reviewer.cjs",
	assign_milestone: "./assign_milestone.cjs",
	assign_to_user: "./assign_to_user.cjs",
	unassign_from_user: "./unassign_from_user.cjs",
	create_code_scanning_alert: "./create_code_scanning_alert.cjs",
	autofix_code_scanning_alert: "./autofix_code_scanning_alert.cjs",
	dispatch_workflow: "./dispatch_workflow.cjs",
	call_workflow: "./call_workflow.cjs",
	create_missing_tool_issue: "./create_missing_tool_issue.cjs",
	missing_tool: "./missing_tool.cjs",
	create_missing_data_issue: "./create_missing_data_issue.cjs",
	missing_data: "./missing_data.cjs",
	noop: "./noop_handler.cjs",
	create_project: "./create_project.cjs",
	create_project_status_update: "./create_project_status_update.cjs",
	update_project: "./update_project.cjs",
};

2. Add call_workflow to compiler handler-manager config generation

Add a call_workflow builder adjacent to dispatch_workflow in pkg/workflow/compiler_safe_outputs_config.go:

"call_workflow": func(cfg *SafeOutputsConfig) map[string]any {
	if cfg.CallWorkflow == nil {
		return nil
	}
	c := cfg.CallWorkflow
	builder := newHandlerConfigBuilder().
		AddTemplatableInt("max", c.Max).
		AddStringSlice("workflows", c.Workflows)

	if len(c.WorkflowFiles) > 0 {
		builder.AddDefault("workflow_files", c.WorkflowFiles)
	}

	return builder.Build()
},

3. Ensure the handler-manager step is generated when call-workflow is configured

Update pkg/workflow/compiler_safe_outputs_job.go:

hasHandlerManagerTypes :=
	data.SafeOutputs.CreateIssues != nil ||
	data.SafeOutputs.CreateDiscussions != nil ||
	data.SafeOutputs.CloseIssues != nil ||
	data.SafeOutputs.CloseDiscussions != nil ||
	data.SafeOutputs.AddLabels != nil ||
	data.SafeOutputs.RemoveLabels != nil ||
	data.SafeOutputs.UpdateIssues != nil ||
	data.SafeOutputs.UpdateDiscussions != nil ||
	data.SafeOutputs.LinkSubIssues != nil ||
	data.SafeOutputs.UpdateRelease != nil ||
	data.SafeOutputs.CreatePullRequestReviewComments != nil ||
	data.SafeOutputs.SubmitPullRequestReview != nil ||
	data.SafeOutputs.ReplyToPullRequestReviewComments != nil ||
	data.SafeOutputs.ResolvePullRequestReviewThreads != nil ||
	data.SafeOutputs.CreatePullRequests != nil ||
	data.SafeOutputs.PushToPullRequestBranch != nil ||
	data.SafeOutputs.UpdatePullRequests != nil ||
	data.SafeOutputs.ClosePullRequests != nil ||
	data.SafeOutputs.MarkPullRequestAsReadyForReview != nil ||
	data.SafeOutputs.HideComment != nil ||
	data.SafeOutputs.SetIssueType != nil ||
	data.SafeOutputs.AddReviewer != nil ||
	data.SafeOutputs.AssignMilestone != nil ||
	data.SafeOutputs.AssignToUser != nil ||
	data.SafeOutputs.UnassignFromUser != nil ||
	data.SafeOutputs.DispatchWorkflow != nil ||
	data.SafeOutputs.CallWorkflow != nil ||
	data.SafeOutputs.CreateCodeScanningAlerts != nil ||
	data.SafeOutputs.AutofixCodeScanningAlert != nil ||
	data.SafeOutputs.MissingTool != nil ||
	data.SafeOutputs.MissingData != nil

This keeps the execution model consistent with the existing design:

  • MCP server registers workflow-specific call_workflow tools
  • agent emits a call_workflow message
  • consolidated handler manager loads call_workflow.cjs
  • call_workflow.cjs sets call_workflow_name / call_workflow_payload
  • compiler-generated conditional uses: jobs consume those outputs

Implementation Plan

  1. Update runtime handler loading

    • Edit actions/setup/js/safe_output_handler_manager.cjs.
    • Add call_workflow: "./call_workflow.cjs" to HANDLER_MAP.
    • Keep call_workflow in the handler-manager path, not STANDALONE_STEP_TYPES, because call_workflow.cjs is the module that sets the downstream action outputs.
  2. Update compiler handler config generation

    • Edit pkg/workflow/compiler_safe_outputs_config.go.
    • Add a handlerRegistry["call_workflow"] builder that emits:
      • max
      • workflows
      • workflow_files
    • Do not add target-repo / target-ref fields here; those belong to dispatch_workflow, not call_workflow.
  3. Update consolidated safe_outputs job gating

    • Edit pkg/workflow/compiler_safe_outputs_job.go.
    • Include data.SafeOutputs.CallWorkflow != nil in hasHandlerManagerTypes so workflows that only route via call-workflow still compile a Process Safe Outputs step.
  4. Add compiler regression tests

    • Edit pkg/workflow/compiler_safe_outputs_config_test.go.
    • Add TestAddHandlerManagerConfigEnvVar_CallWorkflow asserting that GH_AW_SAFE_OUTPUTS_HANDLER_CONFIG contains call_workflow, workflows, and workflow_files when SafeOutputs.CallWorkflow is configured.
    • Edit pkg/workflow/safe_outputs_call_workflow_test.go or pkg/workflow/compiler_safe_outputs_job_test.go.
    • Add TestCallWorkflowOnly_UsesHandlerManagerStep asserting that a workflow with only call-workflow still includes the Process Safe Outputs step.
  5. Add runtime regression tests

    • Edit actions/setup/js/safe_output_handler_manager.test.cjs.
    • Add it("loads call_workflow handler when config.call_workflow is present", ...) that calls loadHandlers() with { call_workflow: { workflows: ["worker-a"], max: 1 } } and asserts the returned map contains call_workflow.
    • Add it("processes call_workflow messages without no-handler warnings", ...) that uses a real loaded handler or a stubbed handler map entry and asserts there is no No handler loaded for type 'call_workflow' result.
  6. Add end-to-end compile/runtime coverage for the exact bug shape

    • Extend pkg/workflow/call_workflow_compilation_test.go with a case that compiles a workflow containing only:
      • safe-outputs.call-workflow
    • Assert the compiled lock file contains:
      • Process Safe Outputs
      • GH_AW_SAFE_OUTPUTS_HANDLER_CONFIG
      • call_workflow
      • downstream call-<worker> fan-out jobs
  7. Validate with the project-standard finish step

    • Run make test at minimum.
    • Prefer make agent-finish before closing the issue implementation, since this change spans both Go compiler output and JS runtime behavior.
  8. No duplicate issue; reference adjacent fixes instead

    • Link this issue to:
    • Clarify that this issue tracks the remaining consolidated-handler-manager gap after those landed.

Reproduction

  1. Use gh-aw v0.58.3.
  2. Create a reusable “gateway” workflow in <org>/<platform-repo> with:
    • on: workflow_call
    • at least one ordinary consolidated safe output (for example add-comment) or only call-workflow
    • safe-outputs.call-workflow pointing at a reusable worker workflow in the same repository
  3. Compile the workflow and install the generated lock file into an application repository.
  4. Trigger the gateway from another repository via workflow_call so the agent selects one of the generated call_workflow tools.
  5. Observe the Process Safe Outputs step fail or warn instead of routing the worker selection.
  6. The exact failing step/error from the reproduced run was:
    • Step: Process Safe Outputs
    • Error: No handler loaded for type 'call_workflow'
  7. Private failing run reference from the original investigation:
    • run 23137133379
    • job 67204124568
    • omitted raw URL because the source repository is private
  8. Compare with current main source:
    • HTTP MCP registration already recognizes _call_workflow_name
    • consolidated handler manager still does not load call_workflow
    • compiler still does not emit call_workflow into GH_AW_SAFE_OUTPUTS_HANDLER_CONFIG

Activity

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

Metadata

Metadata

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions