diff --git a/.changeset/patch-add-safe-output-summaries.md b/.changeset/patch-add-safe-output-summaries.md
new file mode 100644
index 00000000000..52f2922e47d
--- /dev/null
+++ b/.changeset/patch-add-safe-output-summaries.md
@@ -0,0 +1,12 @@
+---
+"gh-aw": patch
+---
+
+Add step summaries for safe-output processing results.
+
+Safe-output handlers now generate collapsible step summaries for each processed
+message, providing visibility into what was created or updated during workflow
+execution. Body previews are truncated at 500 characters to avoid bloat. The
+feature is implemented for both regular safe-outputs and project-based
+safe-outputs via a shared helper module.
+
diff --git a/actions/setup/js/safe_output_handler_manager.cjs b/actions/setup/js/safe_output_handler_manager.cjs
index a58b1c87a1a..5838c4e241a 100644
--- a/actions/setup/js/safe_output_handler_manager.cjs
+++ b/actions/setup/js/safe_output_handler_manager.cjs
@@ -14,6 +14,7 @@ const { getErrorMessage } = require("./error_helpers.cjs");
const { hasUnresolvedTemporaryIds, replaceTemporaryIdReferences, normalizeTemporaryId } = require("./temporary_id.cjs");
const { generateMissingInfoSections } = require("./missing_info_formatter.cjs");
const { setCollectedMissings } = require("./missing_messages_helper.cjs");
+const { writeSafeOutputSummaries } = require("./safe_output_summary.cjs");
const DEFAULT_AGENTIC_CAMPAIGN_LABEL = "agentic-campaign";
@@ -835,6 +836,9 @@ async function main() {
syntheticUpdateCount = await processSyntheticUpdates(github, context, processingResult.outputsWithUnresolvedIds, temporaryIdMap);
}
+ // Write step summaries for all processed safe-outputs
+ await writeSafeOutputSummaries(processingResult.results, agentOutput.items);
+
// Log summary
const successCount = processingResult.results.filter(r => r.success).length;
const failureCount = processingResult.results.filter(r => !r.success && !r.deferred && !r.skipped).length;
diff --git a/actions/setup/js/safe_output_project_handler_manager.cjs b/actions/setup/js/safe_output_project_handler_manager.cjs
index d4b69d7b5ba..3f9bbe477ab 100644
--- a/actions/setup/js/safe_output_project_handler_manager.cjs
+++ b/actions/setup/js/safe_output_project_handler_manager.cjs
@@ -15,6 +15,7 @@
const { loadAgentOutput } = require("./load_agent_output.cjs");
const { getErrorMessage } = require("./error_helpers.cjs");
+const { writeSafeOutputSummaries } = require("./safe_output_summary.cjs");
/**
* Handler map configuration for project-related safe outputs
@@ -226,6 +227,9 @@ async function main() {
// Process messages
const { results, processedCount, temporaryProjectMap } = await processMessages(messageHandlers, messages);
+ // Write step summaries for all processed safe-outputs
+ await writeSafeOutputSummaries(results, messages);
+
// Set outputs
core.setOutput("processed_count", processedCount);
diff --git a/actions/setup/js/safe_output_summary.cjs b/actions/setup/js/safe_output_summary.cjs
new file mode 100644
index 00000000000..a110f8c45aa
--- /dev/null
+++ b/actions/setup/js/safe_output_summary.cjs
@@ -0,0 +1,131 @@
+// @ts-check
+///
+
+/**
+ * Safe Output Summary Generator
+ *
+ * This module provides functionality to generate step summaries for safe-output messages.
+ * Each processed safe-output generates a summary enclosed in a section.
+ */
+
+/**
+ * Generate a step summary for a single safe-output message
+ * @param {Object} options - Summary generation options
+ * @param {string} options.type - The safe-output type (e.g., "create_issue", "create_project")
+ * @param {number} options.messageIndex - The message index (1-based)
+ * @param {boolean} options.success - Whether the message was processed successfully
+ * @param {any} options.result - The result from the handler
+ * @param {any} options.message - The original message
+ * @param {string} [options.error] - Error message if processing failed
+ * @returns {string} - Markdown content for the step summary
+ */
+function generateSafeOutputSummary(options) {
+ const { type, messageIndex, success, result, message, error } = options;
+
+ // Format the type for display (e.g., "create_issue" -> "Create Issue")
+ const displayType = type
+ .split("_")
+ .map(word => word.charAt(0).toUpperCase() + word.slice(1))
+ .join(" ");
+
+ // Choose emoji and status based on success
+ const emoji = success ? "✅" : "❌";
+ const status = success ? "Success" : "Failed";
+
+ // Start building the summary
+ let summary = `\n${emoji} ${displayType} - ${status} (Message ${messageIndex})
\n\n`;
+
+ // Add message details
+ summary += `### ${displayType}\n\n`;
+
+ if (success && result) {
+ // Add result-specific information based on type
+ if (result.url) {
+ summary += `**URL:** ${result.url}\n\n`;
+ }
+ if (result.repo && result.number) {
+ summary += `**Location:** ${result.repo}#${result.number}\n\n`;
+ }
+ if (result.projectUrl) {
+ summary += `**Project URL:** ${result.projectUrl}\n\n`;
+ }
+ if (result.temporaryId) {
+ summary += `**Temporary ID:** \`${result.temporaryId}\`\n\n`;
+ }
+
+ // Add original message details if available
+ if (message) {
+ if (message.title) {
+ summary += `**Title:** ${message.title}\n\n`;
+ }
+ if (message.body && typeof message.body === "string") {
+ // Truncate body if too long
+ const maxBodyLength = 500;
+ const bodyPreview = message.body.length > maxBodyLength ? message.body.substring(0, maxBodyLength) + "..." : message.body;
+ summary += `**Body Preview:**\n\`\`\`\n${bodyPreview}\n\`\`\`\n\n`;
+ }
+ if (message.labels && Array.isArray(message.labels)) {
+ summary += `**Labels:** ${message.labels.join(", ")}\n\n`;
+ }
+ }
+ } else if (error) {
+ // Show error information
+ summary += `**Error:** ${error}\n\n`;
+
+ // Add original message details for debugging
+ if (message) {
+ summary += `**Message Details:**\n\`\`\`json\n${JSON.stringify(message, null, 2).substring(0, 1000)}\n\`\`\`\n\n`;
+ }
+ }
+
+ summary += ` \n\n`;
+
+ return summary;
+}
+
+/**
+ * Write safe-output summaries to the GitHub Actions step summary
+ * @param {Array