Skip to content

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

@johnwilliams-12

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

Metadata

Metadata

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions