diff --git a/.github/workflows/test-claude-add-issue-comment.lock.yml b/.github/workflows/test-claude-add-issue-comment.lock.yml
index 8409d6242de..d67ca37d530 100644
--- a/.github/workflows/test-claude-add-issue-comment.lock.yml
+++ b/.github/workflows/test-claude-add-issue-comment.lock.yml
@@ -428,113 +428,138 @@ jobs:
           GITHUB_AW_SAFE_OUTPUTS_CONFIG: "{\"add-issue-comment\":true}"
         with:
           script: |
-            /**
-             * Sanitizes content for safe output in GitHub Actions
-             * @param {string} content - The content to sanitize
-             * @returns {string} The sanitized content
-             */
-            function sanitizeContent(content) {
-              if (!content || typeof content !== 'string') {
-                return '';
-              }
-              // Read allowed domains from environment variable
-              const allowedDomainsEnv = process.env.GITHUB_AW_ALLOWED_DOMAINS;
-              const defaultAllowedDomains = [
-                'github.com',
-                'github.io',
-                'githubusercontent.com',
-                'githubassets.com',
-                'github.dev',
-                'codespaces.new'
-              ];
-              const allowedDomains = allowedDomainsEnv
-                ? allowedDomainsEnv.split(',').map(d => d.trim()).filter(d => d)
-                : defaultAllowedDomains;
-              let sanitized = content;
-              // Neutralize @mentions to prevent unintended notifications
-              sanitized = neutralizeMentions(sanitized);
-              // Remove control characters (except newlines and tabs)
-              sanitized = sanitized.replace(/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/g, '');
-              // XML character escaping
-              sanitized = sanitized
-                .replace(/&/g, '&amp;')   // Must be first to avoid double-escaping
-                .replace(/</g, '&lt;')
-                .replace(/>/g, '&gt;')
-                .replace(/"/g, '&quot;')
-                .replace(/'/g, '&apos;');
-              // URI filtering - replace non-https protocols with "(redacted)"
-              sanitized = sanitizeUrlProtocols(sanitized);
-              // Domain filtering for HTTPS URIs
-              sanitized = sanitizeUrlDomains(sanitized);
-              // Limit total length to prevent DoS (0.5MB max)
-              const maxLength = 524288;
-              if (sanitized.length > maxLength) {
-                sanitized = sanitized.substring(0, maxLength) + '\n[Content truncated due to length]';
-              }
-              // Limit number of lines to prevent log flooding (65k max)
-              const lines = sanitized.split('\n');
-              const maxLines = 65000;
-              if (lines.length > maxLines) {
-                sanitized = lines.slice(0, maxLines).join('\n') + '\n[Content truncated due to line count]';
-              }
-              // Remove ANSI escape sequences
-              sanitized = sanitized.replace(/\x1b\[[0-9;]*[mGKH]/g, '');
-              // Neutralize common bot trigger phrases
-              sanitized = neutralizeBotTriggers(sanitized);
-              // Trim excessive whitespace
-              return sanitized.trim();
+            async function main() {
+              const fs = require("fs");
               /**
-               * Remove unknown domains
-               * @param {string} s - The string to process
-               * @returns {string} The string with unknown domains redacted
+               * Sanitizes content for safe output in GitHub Actions
+               * @param {string} content - The content to sanitize
+               * @returns {string} The sanitized content
                */
-              function sanitizeUrlDomains(s) {
-                return s.replace(/\bhttps:\/\/([^\/\s\])}'"<>&\x00-\x1f]+)/gi, (match, domain) => {
-                  // Extract the hostname part (before first slash, colon, or other delimiter)
-                  const hostname = domain.split(/[\/:\?#]/)[0].toLowerCase();
-                  // Check if this domain or any parent domain is in the allowlist
-                  const isAllowed = allowedDomains.some(allowedDomain => {
-                    const normalizedAllowed = allowedDomain.toLowerCase();
-                    return hostname === normalizedAllowed || hostname.endsWith('.' + normalizedAllowed);
+              function sanitizeContent(content) {
+                if (!content || typeof content !== 'string') {
+                  return '';
+                }
+                // Read allowed domains from environment variable
+                const allowedDomainsEnv = process.env.GITHUB_AW_ALLOWED_DOMAINS;
+                const defaultAllowedDomains = [
+                  'github.com',
+                  'github.io',
+                  'githubusercontent.com',
+                  'githubassets.com',
+                  'github.dev',
+                  'codespaces.new'
+                ];
+                const allowedDomains = allowedDomainsEnv
+                  ? allowedDomainsEnv.split(',').map(d => d.trim()).filter(d => d)
+                  : defaultAllowedDomains;
+                let sanitized = content;
+                // Neutralize @mentions to prevent unintended notifications
+                sanitized = neutralizeMentions(sanitized);
+                // Remove control characters (except newlines and tabs)
+                sanitized = sanitized.replace(/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/g, '');
+                // XML character escaping
+                sanitized = sanitized
+                  .replace(/&/g, '&amp;')   // Must be first to avoid double-escaping
+                  .replace(/</g, '&lt;')
+                  .replace(/>/g, '&gt;')
+                  .replace(/"/g, '&quot;')
+                  .replace(/'/g, '&apos;');
+                // URI filtering - replace non-https protocols with "(redacted)"
+                sanitized = sanitizeUrlProtocols(sanitized);
+                // Domain filtering for HTTPS URIs
+                sanitized = sanitizeUrlDomains(sanitized);
+                // Limit total length to prevent DoS (0.5MB max)
+                const maxLength = 524288;
+                if (sanitized.length > maxLength) {
+                  sanitized = sanitized.substring(0, maxLength) + '\n[Content truncated due to length]';
+                }
+                // Limit number of lines to prevent log flooding (65k max)
+                const lines = sanitized.split('\n');
+                const maxLines = 65000;
+                if (lines.length > maxLines) {
+                  sanitized = lines.slice(0, maxLines).join('\n') + '\n[Content truncated due to line count]';
+                }
+                // Remove ANSI escape sequences
+                sanitized = sanitized.replace(/\x1b\[[0-9;]*[mGKH]/g, '');
+                // Neutralize common bot trigger phrases
+                sanitized = neutralizeBotTriggers(sanitized);
+                // Trim excessive whitespace
+                return sanitized.trim();
+                /**
+                 * Remove unknown domains
+                 * @param {string} s - The string to process
+                 * @returns {string} The string with unknown domains redacted
+                 */
+                function sanitizeUrlDomains(s) {
+                  return s.replace(/\bhttps:\/\/([^\/\s\])}'"<>&\x00-\x1f]+)/gi, (match, domain) => {
+                    // Extract the hostname part (before first slash, colon, or other delimiter)
+                    const hostname = domain.split(/[\/:\?#]/)[0].toLowerCase();
+                    // Check if this domain or any parent domain is in the allowlist
+                    const isAllowed = allowedDomains.some(allowedDomain => {
+                      const normalizedAllowed = allowedDomain.toLowerCase();
+                      return hostname === normalizedAllowed || hostname.endsWith('.' + normalizedAllowed);
+                    });
+                    return isAllowed ? match : '(redacted)';
                   });
-                  return isAllowed ? match : '(redacted)';
-                });
-              }
-              /**
-               * Remove unknown protocols except https
-               * @param {string} s - The string to process
-               * @returns {string} The string with non-https protocols redacted
-               */
-              function sanitizeUrlProtocols(s) {
-                // Match both protocol:// and protocol: patterns
-                return s.replace(/\b(\w+):(?:\/\/)?[^\s\])}'"<>&\x00-\x1f]+/gi, (match, protocol) => {
-                  // Allow https (case insensitive), redact everything else
-                  return protocol.toLowerCase() === 'https' ? match : '(redacted)';
-                });
-              }
-              /**
-               * Neutralizes @mentions by wrapping them in backticks
-               * @param {string} s - The string to process
-               * @returns {string} The string with neutralized mentions
-               */
-              function neutralizeMentions(s) {
-                // Replace @name or @org/team outside code with `@name`
-                return s.replace(/(^|[^\w`])@([A-Za-z0-9](?:[A-Za-z0-9-]{0,37}[A-Za-z0-9])?(?:\/[A-Za-z0-9._-]+)?)/g,
-                  (_m, p1, p2) => `${p1}\`@${p2}\``);
+                }
+                /**
+                 * Remove unknown protocols except https
+                 * @param {string} s - The string to process
+                 * @returns {string} The string with non-https protocols redacted
+                 */
+                function sanitizeUrlProtocols(s) {
+                  // Match both protocol:// and protocol: patterns
+                  return s.replace(/\b(\w+):(?:\/\/)?[^\s\])}'"<>&\x00-\x1f]+/gi, (match, protocol) => {
+                    // Allow https (case insensitive), redact everything else
+                    return protocol.toLowerCase() === 'https' ? match : '(redacted)';
+                  });
+                }
+                /**
+                 * Neutralizes @mentions by wrapping them in backticks
+                 * @param {string} s - The string to process
+                 * @returns {string} The string with neutralized mentions
+                 */
+                function neutralizeMentions(s) {
+                  // Replace @name or @org/team outside code with `@name`
+                  return s.replace(/(^|[^\w`])@([A-Za-z0-9](?:[A-Za-z0-9-]{0,37}[A-Za-z0-9])?(?:\/[A-Za-z0-9._-]+)?)/g,
+                    (_m, p1, p2) => `${p1}\`@${p2}\``);
+                }
+                /**
+                 * Neutralizes bot trigger phrases by wrapping them in backticks
+                 * @param {string} s - The string to process
+                 * @returns {string} The string with neutralized bot triggers
+                 */
+                function neutralizeBotTriggers(s) {
+                  // Neutralize common bot trigger phrases like "fixes #123", "closes #asdfs", etc.
+                  return s.replace(/\b(fixes?|closes?|resolves?|fix|close|resolve)\s+#(\w+)/gi,
+                    (match, action, ref) => `\`${action} #${ref}\``);
+                }
               }
               /**
-               * Neutralizes bot trigger phrases by wrapping them in backticks
-               * @param {string} s - The string to process
-               * @returns {string} The string with neutralized bot triggers
+               * Gets the maximum allowed count for a given output type
+               * @param {string} itemType - The output item type
+               * @param {Object} config - The safe-outputs configuration
+               * @returns {number} The maximum allowed count
                */
-              function neutralizeBotTriggers(s) {
-                // Neutralize common bot trigger phrases like "fixes #123", "closes #asdfs", etc.
-                return s.replace(/\b(fixes?|closes?|resolves?|fix|close|resolve)\s+#(\w+)/gi,
-                  (match, action, ref) => `\`${action} #${ref}\``);
+              function getMaxAllowedForType(itemType, config) {
+                // Check if max is explicitly specified in config
+                if (config && config[itemType] && typeof config[itemType] === 'object' && config[itemType].max) {
+                  return config[itemType].max;
+                }
+                // Use default limits for plural-supported types
+                switch (itemType) {
+                  case 'create-issue':
+                    return 10; // Allow multiple issues
+                  case 'add-issue-comment':
+                    return 10; // Allow multiple comments
+                  case 'create-pull-request':
+                    return 1;  // Only one pull request allowed
+                  case 'add-issue-labels':
+                    return 1;  // Only one labels operation allowed
+                  default:
+                    return 1;  // Default to single item for unknown types
+                }
               }
-            }
-            async function main() {
-              const fs = require("fs");
               const outputFile = process.env.GITHUB_AW_SAFE_OUTPUTS;
               const safeOutputsConfig = process.env.GITHUB_AW_SAFE_OUTPUTS_CONFIG;
               if (!outputFile) {
@@ -584,10 +609,11 @@ jobs:
                     errors.push(`Line ${i + 1}: Unexpected output type '${itemType}'. Expected one of: ${Object.keys(expectedOutputTypes).join(', ')}`);
                     continue;
                   }
-                  // Check for duplicates based on type (since current format allows only one of each)
-                  const existingItem = parsedItems.find(existing => existing.type === itemType);
-                  if (existingItem) {
-                    errors.push(`Line ${i + 1}: Duplicate output type '${itemType}'. Only one item of each type is currently allowed.`);
+                  // Check for too many items of the same type
+                  const typeCount = parsedItems.filter(existing => existing.type === itemType).length;
+                  const maxAllowed = getMaxAllowedForType(itemType, expectedOutputTypes);
+                  if (typeCount >= maxAllowed) {
+                    errors.push(`Line ${i + 1}: Too many items of type '${itemType}'. Maximum allowed: ${maxAllowed}.`);
                     continue;
                   }
                   // Basic validation based on type
@@ -1089,13 +1115,13 @@ jobs:
                 console.log('No valid items found in agent output');
                 return;
               }
-              // Find the add-issue-comment item
-              const commentItem = validatedOutput.items.find(/** @param {any} item */ item => item.type === 'add-issue-comment');
-              if (!commentItem) {
-                console.log('No add-issue-comment item found in agent output');
+              // Find all add-issue-comment items
+              const commentItems = validatedOutput.items.filter(/** @param {any} item */ item => item.type === 'add-issue-comment');
+              if (commentItems.length === 0) {
+                console.log('No add-issue-comment items found in agent output');
                 return;
               }
-              console.log('Found add-issue-comment item:', { bodyLength: commentItem.body.length });
+              console.log(`Found ${commentItems.length} add-issue-comment item(s)`);
               // Check if we're in an issue or pull request context
               const isIssueContext = context.eventName === 'issues' || context.eventName === 'issue_comment';
               const isPRContext = context.eventName === 'pull_request' || context.eventName === 'pull_request_review' || context.eventName === 'pull_request_review_comment';
@@ -1103,57 +1129,75 @@ jobs:
                 console.log('Not running in issue or pull request context, skipping comment creation');
                 return;
               }
-              // Determine the issue/PR number and comment endpoint
-              let issueNumber;
-              let commentEndpoint;
-              if (isIssueContext) {
-                if (context.payload.issue) {
-                  issueNumber = context.payload.issue.number;
-                  commentEndpoint = 'issues';
-                } else {
-                  console.log('Issue context detected but no issue found in payload');
-                  return;
+              const createdComments = [];
+              // Process each comment item
+              for (let i = 0; i < commentItems.length; i++) {
+                const commentItem = commentItems[i];
+                console.log(`Processing add-issue-comment item ${i + 1}/${commentItems.length}:`, { bodyLength: commentItem.body.length });
+                // Determine the issue/PR number and comment endpoint for this comment
+                let issueNumber;
+                let commentEndpoint;
+                if (isIssueContext) {
+                  if (context.payload.issue) {
+                    issueNumber = context.payload.issue.number;
+                    commentEndpoint = 'issues';
+                  } else {
+                    console.log('Issue context detected but no issue found in payload');
+                    continue;
+                  }
+                } else if (isPRContext) {
+                  if (context.payload.pull_request) {
+                    issueNumber = context.payload.pull_request.number;
+                    commentEndpoint = 'issues'; // PR comments use the issues API endpoint
+                  } else {
+                    console.log('Pull request context detected but no pull request found in payload');
+                    continue;
+                  }
                 }
-              } else if (isPRContext) {
-                if (context.payload.pull_request) {
-                  issueNumber = context.payload.pull_request.number;
-                  commentEndpoint = 'issues'; // PR comments use the issues API endpoint
-                } else {
-                  console.log('Pull request context detected but no pull request found in payload');
-                  return;
+                if (!issueNumber) {
+                  console.log('Could not determine issue or pull request number');
+                  continue;
+                }
+                // Extract body from the JSON item
+                let body = commentItem.body.trim();
+                // Add AI disclaimer with run id, run htmlurl
+                const runId = context.runId;
+                const runUrl = context.payload.repository 
+                  ? `${context.payload.repository.html_url}/actions/runs/${runId}`
+                  : `https://github.com/actions/runs/${runId}`;  
+                body += `\n\n> Generated by Agentic Workflow Run [${runId}](${runUrl})\n`;
+                console.log(`Creating comment on ${commentEndpoint} #${issueNumber}`);
+                console.log('Comment content length:', body.length);
+                try {
+                  // Create the comment using GitHub API
+                  const { data: comment } = await github.rest.issues.createComment({
+                    owner: context.repo.owner,
+                    repo: context.repo.repo,
+                    issue_number: issueNumber,
+                    body: body
+                  });
+                  console.log('Created comment #' + comment.id + ': ' + comment.html_url);
+                  createdComments.push(comment);
+                  // Set output for the last created comment (for backward compatibility)
+                  if (i === commentItems.length - 1) {
+                    core.setOutput('comment_id', comment.id);
+                    core.setOutput('comment_url', comment.html_url);
+                  }
+                } catch (error) {
+                  console.error(`✗ Failed to create comment:`, error instanceof Error ? error.message : String(error));
+                  throw error;
                 }
               }
-              if (!issueNumber) {
-                console.log('Could not determine issue or pull request number');
-                return;
+              // Write summary for all created comments
+              if (createdComments.length > 0) {
+                let summaryContent = '\n\n## GitHub Comments\n';
+                for (const comment of createdComments) {
+                  summaryContent += `- Comment #${comment.id}: [View Comment](${comment.html_url})\n`;
+                }
+                await core.summary.addRaw(summaryContent).write();
               }
-              // Extract body from the JSON item
-              let body = commentItem.body.trim();
-              // Add AI disclaimer with run id, run htmlurl
-              const runId = context.runId;
-              const runUrl = context.payload.repository 
-                ? `${context.payload.repository.html_url}/actions/runs/${runId}`
-                : `https://github.com/actions/runs/${runId}`;  
-              body += `\n\n> Generated by Agentic Workflow Run [${runId}](${runUrl})\n`;
-              console.log(`Creating comment on ${commentEndpoint} #${issueNumber}`);
-              console.log('Comment content length:', body.length);
-              // Create the comment using GitHub API
-              const { data: comment } = await github.rest.issues.createComment({
-                owner: context.repo.owner,
-                repo: context.repo.repo,
-                issue_number: issueNumber,
-                body: body
-              });
-              console.log('Created comment #' + comment.id + ': ' + comment.html_url);
-              // Set output for other jobs to use
-              core.setOutput('comment_id', comment.id);
-              core.setOutput('comment_url', comment.html_url);
-              // write comment id, url to the github_step_summary
-              await core.summary.addRaw(`
-            ## GitHub Comment
-            - Comment ID: ${comment.id}
-            - Comment URL: ${comment.html_url}
-            `).write();
+              console.log(`Successfully created ${createdComments.length} comment(s)`);
+              return createdComments;
             }
             await main();
 
diff --git a/.github/workflows/test-claude-add-issue-labels.lock.yml b/.github/workflows/test-claude-add-issue-labels.lock.yml
index 7b7476b4d28..05a3f89a5c4 100644
--- a/.github/workflows/test-claude-add-issue-labels.lock.yml
+++ b/.github/workflows/test-claude-add-issue-labels.lock.yml
@@ -428,113 +428,138 @@ jobs:
           GITHUB_AW_SAFE_OUTPUTS_CONFIG: "{\"add-issue-labels\":true}"
         with:
           script: |
-            /**
-             * Sanitizes content for safe output in GitHub Actions
-             * @param {string} content - The content to sanitize
-             * @returns {string} The sanitized content
-             */
-            function sanitizeContent(content) {
-              if (!content || typeof content !== 'string') {
-                return '';
-              }
-              // Read allowed domains from environment variable
-              const allowedDomainsEnv = process.env.GITHUB_AW_ALLOWED_DOMAINS;
-              const defaultAllowedDomains = [
-                'github.com',
-                'github.io',
-                'githubusercontent.com',
-                'githubassets.com',
-                'github.dev',
-                'codespaces.new'
-              ];
-              const allowedDomains = allowedDomainsEnv
-                ? allowedDomainsEnv.split(',').map(d => d.trim()).filter(d => d)
-                : defaultAllowedDomains;
-              let sanitized = content;
-              // Neutralize @mentions to prevent unintended notifications
-              sanitized = neutralizeMentions(sanitized);
-              // Remove control characters (except newlines and tabs)
-              sanitized = sanitized.replace(/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/g, '');
-              // XML character escaping
-              sanitized = sanitized
-                .replace(/&/g, '&amp;')   // Must be first to avoid double-escaping
-                .replace(/</g, '&lt;')
-                .replace(/>/g, '&gt;')
-                .replace(/"/g, '&quot;')
-                .replace(/'/g, '&apos;');
-              // URI filtering - replace non-https protocols with "(redacted)"
-              sanitized = sanitizeUrlProtocols(sanitized);
-              // Domain filtering for HTTPS URIs
-              sanitized = sanitizeUrlDomains(sanitized);
-              // Limit total length to prevent DoS (0.5MB max)
-              const maxLength = 524288;
-              if (sanitized.length > maxLength) {
-                sanitized = sanitized.substring(0, maxLength) + '\n[Content truncated due to length]';
-              }
-              // Limit number of lines to prevent log flooding (65k max)
-              const lines = sanitized.split('\n');
-              const maxLines = 65000;
-              if (lines.length > maxLines) {
-                sanitized = lines.slice(0, maxLines).join('\n') + '\n[Content truncated due to line count]';
-              }
-              // Remove ANSI escape sequences
-              sanitized = sanitized.replace(/\x1b\[[0-9;]*[mGKH]/g, '');
-              // Neutralize common bot trigger phrases
-              sanitized = neutralizeBotTriggers(sanitized);
-              // Trim excessive whitespace
-              return sanitized.trim();
+            async function main() {
+              const fs = require("fs");
               /**
-               * Remove unknown domains
-               * @param {string} s - The string to process
-               * @returns {string} The string with unknown domains redacted
+               * Sanitizes content for safe output in GitHub Actions
+               * @param {string} content - The content to sanitize
+               * @returns {string} The sanitized content
                */
-              function sanitizeUrlDomains(s) {
-                return s.replace(/\bhttps:\/\/([^\/\s\])}'"<>&\x00-\x1f]+)/gi, (match, domain) => {
-                  // Extract the hostname part (before first slash, colon, or other delimiter)
-                  const hostname = domain.split(/[\/:\?#]/)[0].toLowerCase();
-                  // Check if this domain or any parent domain is in the allowlist
-                  const isAllowed = allowedDomains.some(allowedDomain => {
-                    const normalizedAllowed = allowedDomain.toLowerCase();
-                    return hostname === normalizedAllowed || hostname.endsWith('.' + normalizedAllowed);
+              function sanitizeContent(content) {
+                if (!content || typeof content !== 'string') {
+                  return '';
+                }
+                // Read allowed domains from environment variable
+                const allowedDomainsEnv = process.env.GITHUB_AW_ALLOWED_DOMAINS;
+                const defaultAllowedDomains = [
+                  'github.com',
+                  'github.io',
+                  'githubusercontent.com',
+                  'githubassets.com',
+                  'github.dev',
+                  'codespaces.new'
+                ];
+                const allowedDomains = allowedDomainsEnv
+                  ? allowedDomainsEnv.split(',').map(d => d.trim()).filter(d => d)
+                  : defaultAllowedDomains;
+                let sanitized = content;
+                // Neutralize @mentions to prevent unintended notifications
+                sanitized = neutralizeMentions(sanitized);
+                // Remove control characters (except newlines and tabs)
+                sanitized = sanitized.replace(/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/g, '');
+                // XML character escaping
+                sanitized = sanitized
+                  .replace(/&/g, '&amp;')   // Must be first to avoid double-escaping
+                  .replace(/</g, '&lt;')
+                  .replace(/>/g, '&gt;')
+                  .replace(/"/g, '&quot;')
+                  .replace(/'/g, '&apos;');
+                // URI filtering - replace non-https protocols with "(redacted)"
+                sanitized = sanitizeUrlProtocols(sanitized);
+                // Domain filtering for HTTPS URIs
+                sanitized = sanitizeUrlDomains(sanitized);
+                // Limit total length to prevent DoS (0.5MB max)
+                const maxLength = 524288;
+                if (sanitized.length > maxLength) {
+                  sanitized = sanitized.substring(0, maxLength) + '\n[Content truncated due to length]';
+                }
+                // Limit number of lines to prevent log flooding (65k max)
+                const lines = sanitized.split('\n');
+                const maxLines = 65000;
+                if (lines.length > maxLines) {
+                  sanitized = lines.slice(0, maxLines).join('\n') + '\n[Content truncated due to line count]';
+                }
+                // Remove ANSI escape sequences
+                sanitized = sanitized.replace(/\x1b\[[0-9;]*[mGKH]/g, '');
+                // Neutralize common bot trigger phrases
+                sanitized = neutralizeBotTriggers(sanitized);
+                // Trim excessive whitespace
+                return sanitized.trim();
+                /**
+                 * Remove unknown domains
+                 * @param {string} s - The string to process
+                 * @returns {string} The string with unknown domains redacted
+                 */
+                function sanitizeUrlDomains(s) {
+                  return s.replace(/\bhttps:\/\/([^\/\s\])}'"<>&\x00-\x1f]+)/gi, (match, domain) => {
+                    // Extract the hostname part (before first slash, colon, or other delimiter)
+                    const hostname = domain.split(/[\/:\?#]/)[0].toLowerCase();
+                    // Check if this domain or any parent domain is in the allowlist
+                    const isAllowed = allowedDomains.some(allowedDomain => {
+                      const normalizedAllowed = allowedDomain.toLowerCase();
+                      return hostname === normalizedAllowed || hostname.endsWith('.' + normalizedAllowed);
+                    });
+                    return isAllowed ? match : '(redacted)';
                   });
-                  return isAllowed ? match : '(redacted)';
-                });
-              }
-              /**
-               * Remove unknown protocols except https
-               * @param {string} s - The string to process
-               * @returns {string} The string with non-https protocols redacted
-               */
-              function sanitizeUrlProtocols(s) {
-                // Match both protocol:// and protocol: patterns
-                return s.replace(/\b(\w+):(?:\/\/)?[^\s\])}'"<>&\x00-\x1f]+/gi, (match, protocol) => {
-                  // Allow https (case insensitive), redact everything else
-                  return protocol.toLowerCase() === 'https' ? match : '(redacted)';
-                });
-              }
-              /**
-               * Neutralizes @mentions by wrapping them in backticks
-               * @param {string} s - The string to process
-               * @returns {string} The string with neutralized mentions
-               */
-              function neutralizeMentions(s) {
-                // Replace @name or @org/team outside code with `@name`
-                return s.replace(/(^|[^\w`])@([A-Za-z0-9](?:[A-Za-z0-9-]{0,37}[A-Za-z0-9])?(?:\/[A-Za-z0-9._-]+)?)/g,
-                  (_m, p1, p2) => `${p1}\`@${p2}\``);
+                }
+                /**
+                 * Remove unknown protocols except https
+                 * @param {string} s - The string to process
+                 * @returns {string} The string with non-https protocols redacted
+                 */
+                function sanitizeUrlProtocols(s) {
+                  // Match both protocol:// and protocol: patterns
+                  return s.replace(/\b(\w+):(?:\/\/)?[^\s\])}'"<>&\x00-\x1f]+/gi, (match, protocol) => {
+                    // Allow https (case insensitive), redact everything else
+                    return protocol.toLowerCase() === 'https' ? match : '(redacted)';
+                  });
+                }
+                /**
+                 * Neutralizes @mentions by wrapping them in backticks
+                 * @param {string} s - The string to process
+                 * @returns {string} The string with neutralized mentions
+                 */
+                function neutralizeMentions(s) {
+                  // Replace @name or @org/team outside code with `@name`
+                  return s.replace(/(^|[^\w`])@([A-Za-z0-9](?:[A-Za-z0-9-]{0,37}[A-Za-z0-9])?(?:\/[A-Za-z0-9._-]+)?)/g,
+                    (_m, p1, p2) => `${p1}\`@${p2}\``);
+                }
+                /**
+                 * Neutralizes bot trigger phrases by wrapping them in backticks
+                 * @param {string} s - The string to process
+                 * @returns {string} The string with neutralized bot triggers
+                 */
+                function neutralizeBotTriggers(s) {
+                  // Neutralize common bot trigger phrases like "fixes #123", "closes #asdfs", etc.
+                  return s.replace(/\b(fixes?|closes?|resolves?|fix|close|resolve)\s+#(\w+)/gi,
+                    (match, action, ref) => `\`${action} #${ref}\``);
+                }
               }
               /**
-               * Neutralizes bot trigger phrases by wrapping them in backticks
-               * @param {string} s - The string to process
-               * @returns {string} The string with neutralized bot triggers
+               * Gets the maximum allowed count for a given output type
+               * @param {string} itemType - The output item type
+               * @param {Object} config - The safe-outputs configuration
+               * @returns {number} The maximum allowed count
                */
-              function neutralizeBotTriggers(s) {
-                // Neutralize common bot trigger phrases like "fixes #123", "closes #asdfs", etc.
-                return s.replace(/\b(fixes?|closes?|resolves?|fix|close|resolve)\s+#(\w+)/gi,
-                  (match, action, ref) => `\`${action} #${ref}\``);
+              function getMaxAllowedForType(itemType, config) {
+                // Check if max is explicitly specified in config
+                if (config && config[itemType] && typeof config[itemType] === 'object' && config[itemType].max) {
+                  return config[itemType].max;
+                }
+                // Use default limits for plural-supported types
+                switch (itemType) {
+                  case 'create-issue':
+                    return 10; // Allow multiple issues
+                  case 'add-issue-comment':
+                    return 10; // Allow multiple comments
+                  case 'create-pull-request':
+                    return 1;  // Only one pull request allowed
+                  case 'add-issue-labels':
+                    return 1;  // Only one labels operation allowed
+                  default:
+                    return 1;  // Default to single item for unknown types
+                }
               }
-            }
-            async function main() {
-              const fs = require("fs");
               const outputFile = process.env.GITHUB_AW_SAFE_OUTPUTS;
               const safeOutputsConfig = process.env.GITHUB_AW_SAFE_OUTPUTS_CONFIG;
               if (!outputFile) {
@@ -584,10 +609,11 @@ jobs:
                     errors.push(`Line ${i + 1}: Unexpected output type '${itemType}'. Expected one of: ${Object.keys(expectedOutputTypes).join(', ')}`);
                     continue;
                   }
-                  // Check for duplicates based on type (since current format allows only one of each)
-                  const existingItem = parsedItems.find(existing => existing.type === itemType);
-                  if (existingItem) {
-                    errors.push(`Line ${i + 1}: Duplicate output type '${itemType}'. Only one item of each type is currently allowed.`);
+                  // Check for too many items of the same type
+                  const typeCount = parsedItems.filter(existing => existing.type === itemType).length;
+                  const maxAllowed = getMaxAllowedForType(itemType, expectedOutputTypes);
+                  if (typeCount >= maxAllowed) {
+                    errors.push(`Line ${i + 1}: Too many items of type '${itemType}'. Maximum allowed: ${maxAllowed}.`);
                     continue;
                   }
                   // Basic validation based on type
diff --git a/.github/workflows/test-claude-alias.lock.yml b/.github/workflows/test-claude-alias.lock.yml
index c4eafa46cfa..8534873fd48 100644
--- a/.github/workflows/test-claude-alias.lock.yml
+++ b/.github/workflows/test-claude-alias.lock.yml
@@ -666,113 +666,138 @@ jobs:
           GITHUB_AW_SAFE_OUTPUTS_CONFIG: "{\"add-issue-comment\":true}"
         with:
           script: |
-            /**
-             * Sanitizes content for safe output in GitHub Actions
-             * @param {string} content - The content to sanitize
-             * @returns {string} The sanitized content
-             */
-            function sanitizeContent(content) {
-              if (!content || typeof content !== 'string') {
-                return '';
-              }
-              // Read allowed domains from environment variable
-              const allowedDomainsEnv = process.env.GITHUB_AW_ALLOWED_DOMAINS;
-              const defaultAllowedDomains = [
-                'github.com',
-                'github.io',
-                'githubusercontent.com',
-                'githubassets.com',
-                'github.dev',
-                'codespaces.new'
-              ];
-              const allowedDomains = allowedDomainsEnv
-                ? allowedDomainsEnv.split(',').map(d => d.trim()).filter(d => d)
-                : defaultAllowedDomains;
-              let sanitized = content;
-              // Neutralize @mentions to prevent unintended notifications
-              sanitized = neutralizeMentions(sanitized);
-              // Remove control characters (except newlines and tabs)
-              sanitized = sanitized.replace(/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/g, '');
-              // XML character escaping
-              sanitized = sanitized
-                .replace(/&/g, '&amp;')   // Must be first to avoid double-escaping
-                .replace(/</g, '&lt;')
-                .replace(/>/g, '&gt;')
-                .replace(/"/g, '&quot;')
-                .replace(/'/g, '&apos;');
-              // URI filtering - replace non-https protocols with "(redacted)"
-              sanitized = sanitizeUrlProtocols(sanitized);
-              // Domain filtering for HTTPS URIs
-              sanitized = sanitizeUrlDomains(sanitized);
-              // Limit total length to prevent DoS (0.5MB max)
-              const maxLength = 524288;
-              if (sanitized.length > maxLength) {
-                sanitized = sanitized.substring(0, maxLength) + '\n[Content truncated due to length]';
-              }
-              // Limit number of lines to prevent log flooding (65k max)
-              const lines = sanitized.split('\n');
-              const maxLines = 65000;
-              if (lines.length > maxLines) {
-                sanitized = lines.slice(0, maxLines).join('\n') + '\n[Content truncated due to line count]';
-              }
-              // Remove ANSI escape sequences
-              sanitized = sanitized.replace(/\x1b\[[0-9;]*[mGKH]/g, '');
-              // Neutralize common bot trigger phrases
-              sanitized = neutralizeBotTriggers(sanitized);
-              // Trim excessive whitespace
-              return sanitized.trim();
+            async function main() {
+              const fs = require("fs");
               /**
-               * Remove unknown domains
-               * @param {string} s - The string to process
-               * @returns {string} The string with unknown domains redacted
+               * Sanitizes content for safe output in GitHub Actions
+               * @param {string} content - The content to sanitize
+               * @returns {string} The sanitized content
                */
-              function sanitizeUrlDomains(s) {
-                return s.replace(/\bhttps:\/\/([^\/\s\])}'"<>&\x00-\x1f]+)/gi, (match, domain) => {
-                  // Extract the hostname part (before first slash, colon, or other delimiter)
-                  const hostname = domain.split(/[\/:\?#]/)[0].toLowerCase();
-                  // Check if this domain or any parent domain is in the allowlist
-                  const isAllowed = allowedDomains.some(allowedDomain => {
-                    const normalizedAllowed = allowedDomain.toLowerCase();
-                    return hostname === normalizedAllowed || hostname.endsWith('.' + normalizedAllowed);
+              function sanitizeContent(content) {
+                if (!content || typeof content !== 'string') {
+                  return '';
+                }
+                // Read allowed domains from environment variable
+                const allowedDomainsEnv = process.env.GITHUB_AW_ALLOWED_DOMAINS;
+                const defaultAllowedDomains = [
+                  'github.com',
+                  'github.io',
+                  'githubusercontent.com',
+                  'githubassets.com',
+                  'github.dev',
+                  'codespaces.new'
+                ];
+                const allowedDomains = allowedDomainsEnv
+                  ? allowedDomainsEnv.split(',').map(d => d.trim()).filter(d => d)
+                  : defaultAllowedDomains;
+                let sanitized = content;
+                // Neutralize @mentions to prevent unintended notifications
+                sanitized = neutralizeMentions(sanitized);
+                // Remove control characters (except newlines and tabs)
+                sanitized = sanitized.replace(/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/g, '');
+                // XML character escaping
+                sanitized = sanitized
+                  .replace(/&/g, '&amp;')   // Must be first to avoid double-escaping
+                  .replace(/</g, '&lt;')
+                  .replace(/>/g, '&gt;')
+                  .replace(/"/g, '&quot;')
+                  .replace(/'/g, '&apos;');
+                // URI filtering - replace non-https protocols with "(redacted)"
+                sanitized = sanitizeUrlProtocols(sanitized);
+                // Domain filtering for HTTPS URIs
+                sanitized = sanitizeUrlDomains(sanitized);
+                // Limit total length to prevent DoS (0.5MB max)
+                const maxLength = 524288;
+                if (sanitized.length > maxLength) {
+                  sanitized = sanitized.substring(0, maxLength) + '\n[Content truncated due to length]';
+                }
+                // Limit number of lines to prevent log flooding (65k max)
+                const lines = sanitized.split('\n');
+                const maxLines = 65000;
+                if (lines.length > maxLines) {
+                  sanitized = lines.slice(0, maxLines).join('\n') + '\n[Content truncated due to line count]';
+                }
+                // Remove ANSI escape sequences
+                sanitized = sanitized.replace(/\x1b\[[0-9;]*[mGKH]/g, '');
+                // Neutralize common bot trigger phrases
+                sanitized = neutralizeBotTriggers(sanitized);
+                // Trim excessive whitespace
+                return sanitized.trim();
+                /**
+                 * Remove unknown domains
+                 * @param {string} s - The string to process
+                 * @returns {string} The string with unknown domains redacted
+                 */
+                function sanitizeUrlDomains(s) {
+                  return s.replace(/\bhttps:\/\/([^\/\s\])}'"<>&\x00-\x1f]+)/gi, (match, domain) => {
+                    // Extract the hostname part (before first slash, colon, or other delimiter)
+                    const hostname = domain.split(/[\/:\?#]/)[0].toLowerCase();
+                    // Check if this domain or any parent domain is in the allowlist
+                    const isAllowed = allowedDomains.some(allowedDomain => {
+                      const normalizedAllowed = allowedDomain.toLowerCase();
+                      return hostname === normalizedAllowed || hostname.endsWith('.' + normalizedAllowed);
+                    });
+                    return isAllowed ? match : '(redacted)';
                   });
-                  return isAllowed ? match : '(redacted)';
-                });
-              }
-              /**
-               * Remove unknown protocols except https
-               * @param {string} s - The string to process
-               * @returns {string} The string with non-https protocols redacted
-               */
-              function sanitizeUrlProtocols(s) {
-                // Match both protocol:// and protocol: patterns
-                return s.replace(/\b(\w+):(?:\/\/)?[^\s\])}'"<>&\x00-\x1f]+/gi, (match, protocol) => {
-                  // Allow https (case insensitive), redact everything else
-                  return protocol.toLowerCase() === 'https' ? match : '(redacted)';
-                });
-              }
-              /**
-               * Neutralizes @mentions by wrapping them in backticks
-               * @param {string} s - The string to process
-               * @returns {string} The string with neutralized mentions
-               */
-              function neutralizeMentions(s) {
-                // Replace @name or @org/team outside code with `@name`
-                return s.replace(/(^|[^\w`])@([A-Za-z0-9](?:[A-Za-z0-9-]{0,37}[A-Za-z0-9])?(?:\/[A-Za-z0-9._-]+)?)/g,
-                  (_m, p1, p2) => `${p1}\`@${p2}\``);
+                }
+                /**
+                 * Remove unknown protocols except https
+                 * @param {string} s - The string to process
+                 * @returns {string} The string with non-https protocols redacted
+                 */
+                function sanitizeUrlProtocols(s) {
+                  // Match both protocol:// and protocol: patterns
+                  return s.replace(/\b(\w+):(?:\/\/)?[^\s\])}'"<>&\x00-\x1f]+/gi, (match, protocol) => {
+                    // Allow https (case insensitive), redact everything else
+                    return protocol.toLowerCase() === 'https' ? match : '(redacted)';
+                  });
+                }
+                /**
+                 * Neutralizes @mentions by wrapping them in backticks
+                 * @param {string} s - The string to process
+                 * @returns {string} The string with neutralized mentions
+                 */
+                function neutralizeMentions(s) {
+                  // Replace @name or @org/team outside code with `@name`
+                  return s.replace(/(^|[^\w`])@([A-Za-z0-9](?:[A-Za-z0-9-]{0,37}[A-Za-z0-9])?(?:\/[A-Za-z0-9._-]+)?)/g,
+                    (_m, p1, p2) => `${p1}\`@${p2}\``);
+                }
+                /**
+                 * Neutralizes bot trigger phrases by wrapping them in backticks
+                 * @param {string} s - The string to process
+                 * @returns {string} The string with neutralized bot triggers
+                 */
+                function neutralizeBotTriggers(s) {
+                  // Neutralize common bot trigger phrases like "fixes #123", "closes #asdfs", etc.
+                  return s.replace(/\b(fixes?|closes?|resolves?|fix|close|resolve)\s+#(\w+)/gi,
+                    (match, action, ref) => `\`${action} #${ref}\``);
+                }
               }
               /**
-               * Neutralizes bot trigger phrases by wrapping them in backticks
-               * @param {string} s - The string to process
-               * @returns {string} The string with neutralized bot triggers
+               * Gets the maximum allowed count for a given output type
+               * @param {string} itemType - The output item type
+               * @param {Object} config - The safe-outputs configuration
+               * @returns {number} The maximum allowed count
                */
-              function neutralizeBotTriggers(s) {
-                // Neutralize common bot trigger phrases like "fixes #123", "closes #asdfs", etc.
-                return s.replace(/\b(fixes?|closes?|resolves?|fix|close|resolve)\s+#(\w+)/gi,
-                  (match, action, ref) => `\`${action} #${ref}\``);
+              function getMaxAllowedForType(itemType, config) {
+                // Check if max is explicitly specified in config
+                if (config && config[itemType] && typeof config[itemType] === 'object' && config[itemType].max) {
+                  return config[itemType].max;
+                }
+                // Use default limits for plural-supported types
+                switch (itemType) {
+                  case 'create-issue':
+                    return 10; // Allow multiple issues
+                  case 'add-issue-comment':
+                    return 10; // Allow multiple comments
+                  case 'create-pull-request':
+                    return 1;  // Only one pull request allowed
+                  case 'add-issue-labels':
+                    return 1;  // Only one labels operation allowed
+                  default:
+                    return 1;  // Default to single item for unknown types
+                }
               }
-            }
-            async function main() {
-              const fs = require("fs");
               const outputFile = process.env.GITHUB_AW_SAFE_OUTPUTS;
               const safeOutputsConfig = process.env.GITHUB_AW_SAFE_OUTPUTS_CONFIG;
               if (!outputFile) {
@@ -822,10 +847,11 @@ jobs:
                     errors.push(`Line ${i + 1}: Unexpected output type '${itemType}'. Expected one of: ${Object.keys(expectedOutputTypes).join(', ')}`);
                     continue;
                   }
-                  // Check for duplicates based on type (since current format allows only one of each)
-                  const existingItem = parsedItems.find(existing => existing.type === itemType);
-                  if (existingItem) {
-                    errors.push(`Line ${i + 1}: Duplicate output type '${itemType}'. Only one item of each type is currently allowed.`);
+                  // Check for too many items of the same type
+                  const typeCount = parsedItems.filter(existing => existing.type === itemType).length;
+                  const maxAllowed = getMaxAllowedForType(itemType, expectedOutputTypes);
+                  if (typeCount >= maxAllowed) {
+                    errors.push(`Line ${i + 1}: Too many items of type '${itemType}'. Maximum allowed: ${maxAllowed}.`);
                     continue;
                   }
                   // Basic validation based on type
@@ -1327,13 +1353,13 @@ jobs:
                 console.log('No valid items found in agent output');
                 return;
               }
-              // Find the add-issue-comment item
-              const commentItem = validatedOutput.items.find(/** @param {any} item */ item => item.type === 'add-issue-comment');
-              if (!commentItem) {
-                console.log('No add-issue-comment item found in agent output');
+              // Find all add-issue-comment items
+              const commentItems = validatedOutput.items.filter(/** @param {any} item */ item => item.type === 'add-issue-comment');
+              if (commentItems.length === 0) {
+                console.log('No add-issue-comment items found in agent output');
                 return;
               }
-              console.log('Found add-issue-comment item:', { bodyLength: commentItem.body.length });
+              console.log(`Found ${commentItems.length} add-issue-comment item(s)`);
               // Check if we're in an issue or pull request context
               const isIssueContext = context.eventName === 'issues' || context.eventName === 'issue_comment';
               const isPRContext = context.eventName === 'pull_request' || context.eventName === 'pull_request_review' || context.eventName === 'pull_request_review_comment';
@@ -1341,57 +1367,75 @@ jobs:
                 console.log('Not running in issue or pull request context, skipping comment creation');
                 return;
               }
-              // Determine the issue/PR number and comment endpoint
-              let issueNumber;
-              let commentEndpoint;
-              if (isIssueContext) {
-                if (context.payload.issue) {
-                  issueNumber = context.payload.issue.number;
-                  commentEndpoint = 'issues';
-                } else {
-                  console.log('Issue context detected but no issue found in payload');
-                  return;
+              const createdComments = [];
+              // Process each comment item
+              for (let i = 0; i < commentItems.length; i++) {
+                const commentItem = commentItems[i];
+                console.log(`Processing add-issue-comment item ${i + 1}/${commentItems.length}:`, { bodyLength: commentItem.body.length });
+                // Determine the issue/PR number and comment endpoint for this comment
+                let issueNumber;
+                let commentEndpoint;
+                if (isIssueContext) {
+                  if (context.payload.issue) {
+                    issueNumber = context.payload.issue.number;
+                    commentEndpoint = 'issues';
+                  } else {
+                    console.log('Issue context detected but no issue found in payload');
+                    continue;
+                  }
+                } else if (isPRContext) {
+                  if (context.payload.pull_request) {
+                    issueNumber = context.payload.pull_request.number;
+                    commentEndpoint = 'issues'; // PR comments use the issues API endpoint
+                  } else {
+                    console.log('Pull request context detected but no pull request found in payload');
+                    continue;
+                  }
                 }
-              } else if (isPRContext) {
-                if (context.payload.pull_request) {
-                  issueNumber = context.payload.pull_request.number;
-                  commentEndpoint = 'issues'; // PR comments use the issues API endpoint
-                } else {
-                  console.log('Pull request context detected but no pull request found in payload');
-                  return;
+                if (!issueNumber) {
+                  console.log('Could not determine issue or pull request number');
+                  continue;
+                }
+                // Extract body from the JSON item
+                let body = commentItem.body.trim();
+                // Add AI disclaimer with run id, run htmlurl
+                const runId = context.runId;
+                const runUrl = context.payload.repository 
+                  ? `${context.payload.repository.html_url}/actions/runs/${runId}`
+                  : `https://github.com/actions/runs/${runId}`;  
+                body += `\n\n> Generated by Agentic Workflow Run [${runId}](${runUrl})\n`;
+                console.log(`Creating comment on ${commentEndpoint} #${issueNumber}`);
+                console.log('Comment content length:', body.length);
+                try {
+                  // Create the comment using GitHub API
+                  const { data: comment } = await github.rest.issues.createComment({
+                    owner: context.repo.owner,
+                    repo: context.repo.repo,
+                    issue_number: issueNumber,
+                    body: body
+                  });
+                  console.log('Created comment #' + comment.id + ': ' + comment.html_url);
+                  createdComments.push(comment);
+                  // Set output for the last created comment (for backward compatibility)
+                  if (i === commentItems.length - 1) {
+                    core.setOutput('comment_id', comment.id);
+                    core.setOutput('comment_url', comment.html_url);
+                  }
+                } catch (error) {
+                  console.error(`✗ Failed to create comment:`, error instanceof Error ? error.message : String(error));
+                  throw error;
                 }
               }
-              if (!issueNumber) {
-                console.log('Could not determine issue or pull request number');
-                return;
+              // Write summary for all created comments
+              if (createdComments.length > 0) {
+                let summaryContent = '\n\n## GitHub Comments\n';
+                for (const comment of createdComments) {
+                  summaryContent += `- Comment #${comment.id}: [View Comment](${comment.html_url})\n`;
+                }
+                await core.summary.addRaw(summaryContent).write();
               }
-              // Extract body from the JSON item
-              let body = commentItem.body.trim();
-              // Add AI disclaimer with run id, run htmlurl
-              const runId = context.runId;
-              const runUrl = context.payload.repository 
-                ? `${context.payload.repository.html_url}/actions/runs/${runId}`
-                : `https://github.com/actions/runs/${runId}`;  
-              body += `\n\n> Generated by Agentic Workflow Run [${runId}](${runUrl})\n`;
-              console.log(`Creating comment on ${commentEndpoint} #${issueNumber}`);
-              console.log('Comment content length:', body.length);
-              // Create the comment using GitHub API
-              const { data: comment } = await github.rest.issues.createComment({
-                owner: context.repo.owner,
-                repo: context.repo.repo,
-                issue_number: issueNumber,
-                body: body
-              });
-              console.log('Created comment #' + comment.id + ': ' + comment.html_url);
-              // Set output for other jobs to use
-              core.setOutput('comment_id', comment.id);
-              core.setOutput('comment_url', comment.html_url);
-              // write comment id, url to the github_step_summary
-              await core.summary.addRaw(`
-            ## GitHub Comment
-            - Comment ID: ${comment.id}
-            - Comment URL: ${comment.html_url}
-            `).write();
+              console.log(`Successfully created ${createdComments.length} comment(s)`);
+              return createdComments;
             }
             await main();
 
diff --git a/.github/workflows/test-claude-create-issue.lock.yml b/.github/workflows/test-claude-create-issue.lock.yml
index 96785e1245f..e4032116904 100644
--- a/.github/workflows/test-claude-create-issue.lock.yml
+++ b/.github/workflows/test-claude-create-issue.lock.yml
@@ -257,113 +257,138 @@ jobs:
           GITHUB_AW_SAFE_OUTPUTS_CONFIG: "{\"create-issue\":true}"
         with:
           script: |
-            /**
-             * Sanitizes content for safe output in GitHub Actions
-             * @param {string} content - The content to sanitize
-             * @returns {string} The sanitized content
-             */
-            function sanitizeContent(content) {
-              if (!content || typeof content !== 'string') {
-                return '';
-              }
-              // Read allowed domains from environment variable
-              const allowedDomainsEnv = process.env.GITHUB_AW_ALLOWED_DOMAINS;
-              const defaultAllowedDomains = [
-                'github.com',
-                'github.io',
-                'githubusercontent.com',
-                'githubassets.com',
-                'github.dev',
-                'codespaces.new'
-              ];
-              const allowedDomains = allowedDomainsEnv
-                ? allowedDomainsEnv.split(',').map(d => d.trim()).filter(d => d)
-                : defaultAllowedDomains;
-              let sanitized = content;
-              // Neutralize @mentions to prevent unintended notifications
-              sanitized = neutralizeMentions(sanitized);
-              // Remove control characters (except newlines and tabs)
-              sanitized = sanitized.replace(/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/g, '');
-              // XML character escaping
-              sanitized = sanitized
-                .replace(/&/g, '&amp;')   // Must be first to avoid double-escaping
-                .replace(/</g, '&lt;')
-                .replace(/>/g, '&gt;')
-                .replace(/"/g, '&quot;')
-                .replace(/'/g, '&apos;');
-              // URI filtering - replace non-https protocols with "(redacted)"
-              sanitized = sanitizeUrlProtocols(sanitized);
-              // Domain filtering for HTTPS URIs
-              sanitized = sanitizeUrlDomains(sanitized);
-              // Limit total length to prevent DoS (0.5MB max)
-              const maxLength = 524288;
-              if (sanitized.length > maxLength) {
-                sanitized = sanitized.substring(0, maxLength) + '\n[Content truncated due to length]';
-              }
-              // Limit number of lines to prevent log flooding (65k max)
-              const lines = sanitized.split('\n');
-              const maxLines = 65000;
-              if (lines.length > maxLines) {
-                sanitized = lines.slice(0, maxLines).join('\n') + '\n[Content truncated due to line count]';
-              }
-              // Remove ANSI escape sequences
-              sanitized = sanitized.replace(/\x1b\[[0-9;]*[mGKH]/g, '');
-              // Neutralize common bot trigger phrases
-              sanitized = neutralizeBotTriggers(sanitized);
-              // Trim excessive whitespace
-              return sanitized.trim();
+            async function main() {
+              const fs = require("fs");
               /**
-               * Remove unknown domains
-               * @param {string} s - The string to process
-               * @returns {string} The string with unknown domains redacted
+               * Sanitizes content for safe output in GitHub Actions
+               * @param {string} content - The content to sanitize
+               * @returns {string} The sanitized content
                */
-              function sanitizeUrlDomains(s) {
-                return s.replace(/\bhttps:\/\/([^\/\s\])}'"<>&\x00-\x1f]+)/gi, (match, domain) => {
-                  // Extract the hostname part (before first slash, colon, or other delimiter)
-                  const hostname = domain.split(/[\/:\?#]/)[0].toLowerCase();
-                  // Check if this domain or any parent domain is in the allowlist
-                  const isAllowed = allowedDomains.some(allowedDomain => {
-                    const normalizedAllowed = allowedDomain.toLowerCase();
-                    return hostname === normalizedAllowed || hostname.endsWith('.' + normalizedAllowed);
+              function sanitizeContent(content) {
+                if (!content || typeof content !== 'string') {
+                  return '';
+                }
+                // Read allowed domains from environment variable
+                const allowedDomainsEnv = process.env.GITHUB_AW_ALLOWED_DOMAINS;
+                const defaultAllowedDomains = [
+                  'github.com',
+                  'github.io',
+                  'githubusercontent.com',
+                  'githubassets.com',
+                  'github.dev',
+                  'codespaces.new'
+                ];
+                const allowedDomains = allowedDomainsEnv
+                  ? allowedDomainsEnv.split(',').map(d => d.trim()).filter(d => d)
+                  : defaultAllowedDomains;
+                let sanitized = content;
+                // Neutralize @mentions to prevent unintended notifications
+                sanitized = neutralizeMentions(sanitized);
+                // Remove control characters (except newlines and tabs)
+                sanitized = sanitized.replace(/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/g, '');
+                // XML character escaping
+                sanitized = sanitized
+                  .replace(/&/g, '&amp;')   // Must be first to avoid double-escaping
+                  .replace(/</g, '&lt;')
+                  .replace(/>/g, '&gt;')
+                  .replace(/"/g, '&quot;')
+                  .replace(/'/g, '&apos;');
+                // URI filtering - replace non-https protocols with "(redacted)"
+                sanitized = sanitizeUrlProtocols(sanitized);
+                // Domain filtering for HTTPS URIs
+                sanitized = sanitizeUrlDomains(sanitized);
+                // Limit total length to prevent DoS (0.5MB max)
+                const maxLength = 524288;
+                if (sanitized.length > maxLength) {
+                  sanitized = sanitized.substring(0, maxLength) + '\n[Content truncated due to length]';
+                }
+                // Limit number of lines to prevent log flooding (65k max)
+                const lines = sanitized.split('\n');
+                const maxLines = 65000;
+                if (lines.length > maxLines) {
+                  sanitized = lines.slice(0, maxLines).join('\n') + '\n[Content truncated due to line count]';
+                }
+                // Remove ANSI escape sequences
+                sanitized = sanitized.replace(/\x1b\[[0-9;]*[mGKH]/g, '');
+                // Neutralize common bot trigger phrases
+                sanitized = neutralizeBotTriggers(sanitized);
+                // Trim excessive whitespace
+                return sanitized.trim();
+                /**
+                 * Remove unknown domains
+                 * @param {string} s - The string to process
+                 * @returns {string} The string with unknown domains redacted
+                 */
+                function sanitizeUrlDomains(s) {
+                  return s.replace(/\bhttps:\/\/([^\/\s\])}'"<>&\x00-\x1f]+)/gi, (match, domain) => {
+                    // Extract the hostname part (before first slash, colon, or other delimiter)
+                    const hostname = domain.split(/[\/:\?#]/)[0].toLowerCase();
+                    // Check if this domain or any parent domain is in the allowlist
+                    const isAllowed = allowedDomains.some(allowedDomain => {
+                      const normalizedAllowed = allowedDomain.toLowerCase();
+                      return hostname === normalizedAllowed || hostname.endsWith('.' + normalizedAllowed);
+                    });
+                    return isAllowed ? match : '(redacted)';
                   });
-                  return isAllowed ? match : '(redacted)';
-                });
-              }
-              /**
-               * Remove unknown protocols except https
-               * @param {string} s - The string to process
-               * @returns {string} The string with non-https protocols redacted
-               */
-              function sanitizeUrlProtocols(s) {
-                // Match both protocol:// and protocol: patterns
-                return s.replace(/\b(\w+):(?:\/\/)?[^\s\])}'"<>&\x00-\x1f]+/gi, (match, protocol) => {
-                  // Allow https (case insensitive), redact everything else
-                  return protocol.toLowerCase() === 'https' ? match : '(redacted)';
-                });
-              }
-              /**
-               * Neutralizes @mentions by wrapping them in backticks
-               * @param {string} s - The string to process
-               * @returns {string} The string with neutralized mentions
-               */
-              function neutralizeMentions(s) {
-                // Replace @name or @org/team outside code with `@name`
-                return s.replace(/(^|[^\w`])@([A-Za-z0-9](?:[A-Za-z0-9-]{0,37}[A-Za-z0-9])?(?:\/[A-Za-z0-9._-]+)?)/g,
-                  (_m, p1, p2) => `${p1}\`@${p2}\``);
+                }
+                /**
+                 * Remove unknown protocols except https
+                 * @param {string} s - The string to process
+                 * @returns {string} The string with non-https protocols redacted
+                 */
+                function sanitizeUrlProtocols(s) {
+                  // Match both protocol:// and protocol: patterns
+                  return s.replace(/\b(\w+):(?:\/\/)?[^\s\])}'"<>&\x00-\x1f]+/gi, (match, protocol) => {
+                    // Allow https (case insensitive), redact everything else
+                    return protocol.toLowerCase() === 'https' ? match : '(redacted)';
+                  });
+                }
+                /**
+                 * Neutralizes @mentions by wrapping them in backticks
+                 * @param {string} s - The string to process
+                 * @returns {string} The string with neutralized mentions
+                 */
+                function neutralizeMentions(s) {
+                  // Replace @name or @org/team outside code with `@name`
+                  return s.replace(/(^|[^\w`])@([A-Za-z0-9](?:[A-Za-z0-9-]{0,37}[A-Za-z0-9])?(?:\/[A-Za-z0-9._-]+)?)/g,
+                    (_m, p1, p2) => `${p1}\`@${p2}\``);
+                }
+                /**
+                 * Neutralizes bot trigger phrases by wrapping them in backticks
+                 * @param {string} s - The string to process
+                 * @returns {string} The string with neutralized bot triggers
+                 */
+                function neutralizeBotTriggers(s) {
+                  // Neutralize common bot trigger phrases like "fixes #123", "closes #asdfs", etc.
+                  return s.replace(/\b(fixes?|closes?|resolves?|fix|close|resolve)\s+#(\w+)/gi,
+                    (match, action, ref) => `\`${action} #${ref}\``);
+                }
               }
               /**
-               * Neutralizes bot trigger phrases by wrapping them in backticks
-               * @param {string} s - The string to process
-               * @returns {string} The string with neutralized bot triggers
+               * Gets the maximum allowed count for a given output type
+               * @param {string} itemType - The output item type
+               * @param {Object} config - The safe-outputs configuration
+               * @returns {number} The maximum allowed count
                */
-              function neutralizeBotTriggers(s) {
-                // Neutralize common bot trigger phrases like "fixes #123", "closes #asdfs", etc.
-                return s.replace(/\b(fixes?|closes?|resolves?|fix|close|resolve)\s+#(\w+)/gi,
-                  (match, action, ref) => `\`${action} #${ref}\``);
+              function getMaxAllowedForType(itemType, config) {
+                // Check if max is explicitly specified in config
+                if (config && config[itemType] && typeof config[itemType] === 'object' && config[itemType].max) {
+                  return config[itemType].max;
+                }
+                // Use default limits for plural-supported types
+                switch (itemType) {
+                  case 'create-issue':
+                    return 10; // Allow multiple issues
+                  case 'add-issue-comment':
+                    return 10; // Allow multiple comments
+                  case 'create-pull-request':
+                    return 1;  // Only one pull request allowed
+                  case 'add-issue-labels':
+                    return 1;  // Only one labels operation allowed
+                  default:
+                    return 1;  // Default to single item for unknown types
+                }
               }
-            }
-            async function main() {
-              const fs = require("fs");
               const outputFile = process.env.GITHUB_AW_SAFE_OUTPUTS;
               const safeOutputsConfig = process.env.GITHUB_AW_SAFE_OUTPUTS_CONFIG;
               if (!outputFile) {
@@ -413,10 +438,11 @@ jobs:
                     errors.push(`Line ${i + 1}: Unexpected output type '${itemType}'. Expected one of: ${Object.keys(expectedOutputTypes).join(', ')}`);
                     continue;
                   }
-                  // Check for duplicates based on type (since current format allows only one of each)
-                  const existingItem = parsedItems.find(existing => existing.type === itemType);
-                  if (existingItem) {
-                    errors.push(`Line ${i + 1}: Duplicate output type '${itemType}'. Only one item of each type is currently allowed.`);
+                  // Check for too many items of the same type
+                  const typeCount = parsedItems.filter(existing => existing.type === itemType).length;
+                  const maxAllowed = getMaxAllowedForType(itemType, expectedOutputTypes);
+                  if (typeCount >= maxAllowed) {
+                    errors.push(`Line ${i + 1}: Too many items of type '${itemType}'. Maximum allowed: ${maxAllowed}.`);
                     continue;
                   }
                   // Basic validation based on type
@@ -918,83 +944,101 @@ jobs:
                 console.log('No valid items found in agent output');
                 return;
               }
-              // Find the create-issue item
-              const createIssueItem = validatedOutput.items.find(/** @param {any} item */ item => item.type === 'create-issue');
-              if (!createIssueItem) {
-                console.log('No create-issue item found in agent output');
+              // Find all create-issue items
+              const createIssueItems = validatedOutput.items.filter(/** @param {any} item */ item => item.type === 'create-issue');
+              if (createIssueItems.length === 0) {
+                console.log('No create-issue items found in agent output');
                 return;
               }
-              console.log('Found create-issue item:', { title: createIssueItem.title, bodyLength: createIssueItem.body.length });
+              console.log(`Found ${createIssueItems.length} create-issue item(s)`);
               // Check if we're in an issue context (triggered by an issue event)
               const parentIssueNumber = context.payload?.issue?.number;
-              // Parse labels from environment variable (comma-separated string) or from the JSON item
+              // Parse labels from environment variable (comma-separated string)
               const labelsEnv = process.env.GITHUB_AW_ISSUE_LABELS;
-              let labels = labelsEnv ? labelsEnv.split(',').map(/** @param {string} label */ label => label.trim()).filter(/** @param {string} label */ label => label) : [];
-              // If the item has labels, use those instead (or merge them)
-              if (createIssueItem.labels && Array.isArray(createIssueItem.labels)) {
-                labels = [...labels, ...createIssueItem.labels].filter(Boolean);
-              }
-              // Extract title and body from the JSON item
-              let title = createIssueItem.title ? createIssueItem.title.trim() : '';
-              let bodyLines = createIssueItem.body.split('\n');
-              // If no title was found, use the body content as title (or a default)
-              if (!title) {
-                title = createIssueItem.body || 'Agent Output';
-              }
-              // Apply title prefix if provided via environment variable
-              const titlePrefix = process.env.GITHUB_AW_ISSUE_TITLE_PREFIX;
-              if (titlePrefix && !title.startsWith(titlePrefix)) {
-                title = titlePrefix + title;
-              }
-              if (parentIssueNumber) {
-                console.log('Detected issue context, parent issue #' + parentIssueNumber);
-                // Add reference to parent issue in the child issue body
-                bodyLines.push(`Related to #${parentIssueNumber}`);
-              }
-              // Add AI disclaimer with run id, run htmlurl
-              // Add AI disclaimer with workflow run information
-              const runId = context.runId;
-              const runUrl = context.payload.repository 
-                ? `${context.payload.repository.html_url}/actions/runs/${runId}`
-                : `https://github.com/actions/runs/${runId}`;  
-              bodyLines.push(``, ``, `> Generated by Agentic Workflow Run [${runId}](${runUrl})`, '');
-              // Prepare the body content
-              const body = bodyLines.join('\n').trim();
-              console.log('Creating issue with title:', title);
-              console.log('Labels:', labels);
-              console.log('Body length:', body.length);
-              // Create the issue using GitHub API
-              const { data: issue } = await github.rest.issues.create({
-                owner: context.repo.owner,
-                repo: context.repo.repo,
-                title: title,
-                body: body,
-                labels: labels
-              });
-              console.log('Created issue #' + issue.number + ': ' + issue.html_url);
-              // If we have a parent issue, add a comment to it referencing the new child issue
-              if (parentIssueNumber) {
+              let envLabels = labelsEnv ? labelsEnv.split(',').map(/** @param {string} label */ label => label.trim()).filter(/** @param {string} label */ label => label) : [];
+              const createdIssues = [];
+              // Process each create-issue item
+              for (let i = 0; i < createIssueItems.length; i++) {
+                const createIssueItem = createIssueItems[i];
+                console.log(`Processing create-issue item ${i + 1}/${createIssueItems.length}:`, { title: createIssueItem.title, bodyLength: createIssueItem.body.length });
+                // Merge environment labels with item-specific labels
+                let labels = [...envLabels];
+                if (createIssueItem.labels && Array.isArray(createIssueItem.labels)) {
+                  labels = [...labels, ...createIssueItem.labels].filter(Boolean);
+                }
+                // Extract title and body from the JSON item
+                let title = createIssueItem.title ? createIssueItem.title.trim() : '';
+                let bodyLines = createIssueItem.body.split('\n');
+                // If no title was found, use the body content as title (or a default)
+                if (!title) {
+                  title = createIssueItem.body || 'Agent Output';
+                }
+                // Apply title prefix if provided via environment variable
+                const titlePrefix = process.env.GITHUB_AW_ISSUE_TITLE_PREFIX;
+                if (titlePrefix && !title.startsWith(titlePrefix)) {
+                  title = titlePrefix + title;
+                }
+                if (parentIssueNumber) {
+                  console.log('Detected issue context, parent issue #' + parentIssueNumber);
+                  // Add reference to parent issue in the child issue body
+                  bodyLines.push(`Related to #${parentIssueNumber}`);
+                }
+                // Add AI disclaimer with run id, run htmlurl
+                // Add AI disclaimer with workflow run information
+                const runId = context.runId;
+                const runUrl = context.payload.repository 
+                  ? `${context.payload.repository.html_url}/actions/runs/${runId}`
+                  : `https://github.com/actions/runs/${runId}`;  
+                bodyLines.push(``, ``, `> Generated by Agentic Workflow Run [${runId}](${runUrl})`, '');
+                // Prepare the body content
+                const body = bodyLines.join('\n').trim();
+                console.log('Creating issue with title:', title);
+                console.log('Labels:', labels);
+                console.log('Body length:', body.length);
                 try {
-                  await github.rest.issues.createComment({
+                  // Create the issue using GitHub API
+                  const { data: issue } = await github.rest.issues.create({
                     owner: context.repo.owner,
                     repo: context.repo.repo,
-                    issue_number: parentIssueNumber,
-                    body: `Created related issue: #${issue.number}`
+                    title: title,
+                    body: body,
+                    labels: labels
                   });
-                  console.log('Added comment to parent issue #' + parentIssueNumber);
+                  console.log('Created issue #' + issue.number + ': ' + issue.html_url);
+                  createdIssues.push(issue);
+                  // If we have a parent issue, add a comment to it referencing the new child issue
+                  if (parentIssueNumber) {
+                    try {
+                      await github.rest.issues.createComment({
+                        owner: context.repo.owner,
+                        repo: context.repo.repo,
+                        issue_number: parentIssueNumber,
+                        body: `Created related issue: #${issue.number}`
+                      });
+                      console.log('Added comment to parent issue #' + parentIssueNumber);
+                    } catch (error) {
+                      console.log('Warning: Could not add comment to parent issue:', error instanceof Error ? error.message : String(error));
+                    }
+                  }
+                  // Set output for the last created issue (for backward compatibility)
+                  if (i === createIssueItems.length - 1) {
+                    core.setOutput('issue_number', issue.number);
+                    core.setOutput('issue_url', issue.html_url);
+                  }
                 } catch (error) {
-                  console.log('Warning: Could not add comment to parent issue:', error instanceof Error ? error.message : String(error));
+                  console.error(`✗ Failed to create issue "${title}":`, error instanceof Error ? error.message : String(error));
+                  throw error;
+                }
+              }
+              // Write summary for all created issues
+              if (createdIssues.length > 0) {
+                let summaryContent = '\n\n## GitHub Issues\n';
+                for (const issue of createdIssues) {
+                  summaryContent += `- Issue #${issue.number}: [${issue.title}](${issue.html_url})\n`;
                 }
+                await core.summary.addRaw(summaryContent).write();
               }
-              // Set output for other jobs to use
-              core.setOutput('issue_number', issue.number);
-              core.setOutput('issue_url', issue.html_url);
-              // write issue to summary
-              await core.summary.addRaw(`
-            ## GitHub Issue
-            - Issue ID: ${issue.number}
-            - Issue URL: ${issue.html_url}
-            `).write();
+              console.log(`Successfully created ${createdIssues.length} issue(s)`);
             }
             await main();
 
diff --git a/.github/workflows/test-claude-create-pull-request.lock.yml b/.github/workflows/test-claude-create-pull-request.lock.yml
index 8ff2685f769..5e26ffc5b45 100644
--- a/.github/workflows/test-claude-create-pull-request.lock.yml
+++ b/.github/workflows/test-claude-create-pull-request.lock.yml
@@ -266,113 +266,138 @@ jobs:
           GITHUB_AW_SAFE_OUTPUTS_CONFIG: "{\"create-pull-request\":true}"
         with:
           script: |
-            /**
-             * Sanitizes content for safe output in GitHub Actions
-             * @param {string} content - The content to sanitize
-             * @returns {string} The sanitized content
-             */
-            function sanitizeContent(content) {
-              if (!content || typeof content !== 'string') {
-                return '';
-              }
-              // Read allowed domains from environment variable
-              const allowedDomainsEnv = process.env.GITHUB_AW_ALLOWED_DOMAINS;
-              const defaultAllowedDomains = [
-                'github.com',
-                'github.io',
-                'githubusercontent.com',
-                'githubassets.com',
-                'github.dev',
-                'codespaces.new'
-              ];
-              const allowedDomains = allowedDomainsEnv
-                ? allowedDomainsEnv.split(',').map(d => d.trim()).filter(d => d)
-                : defaultAllowedDomains;
-              let sanitized = content;
-              // Neutralize @mentions to prevent unintended notifications
-              sanitized = neutralizeMentions(sanitized);
-              // Remove control characters (except newlines and tabs)
-              sanitized = sanitized.replace(/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/g, '');
-              // XML character escaping
-              sanitized = sanitized
-                .replace(/&/g, '&amp;')   // Must be first to avoid double-escaping
-                .replace(/</g, '&lt;')
-                .replace(/>/g, '&gt;')
-                .replace(/"/g, '&quot;')
-                .replace(/'/g, '&apos;');
-              // URI filtering - replace non-https protocols with "(redacted)"
-              sanitized = sanitizeUrlProtocols(sanitized);
-              // Domain filtering for HTTPS URIs
-              sanitized = sanitizeUrlDomains(sanitized);
-              // Limit total length to prevent DoS (0.5MB max)
-              const maxLength = 524288;
-              if (sanitized.length > maxLength) {
-                sanitized = sanitized.substring(0, maxLength) + '\n[Content truncated due to length]';
-              }
-              // Limit number of lines to prevent log flooding (65k max)
-              const lines = sanitized.split('\n');
-              const maxLines = 65000;
-              if (lines.length > maxLines) {
-                sanitized = lines.slice(0, maxLines).join('\n') + '\n[Content truncated due to line count]';
-              }
-              // Remove ANSI escape sequences
-              sanitized = sanitized.replace(/\x1b\[[0-9;]*[mGKH]/g, '');
-              // Neutralize common bot trigger phrases
-              sanitized = neutralizeBotTriggers(sanitized);
-              // Trim excessive whitespace
-              return sanitized.trim();
+            async function main() {
+              const fs = require("fs");
               /**
-               * Remove unknown domains
-               * @param {string} s - The string to process
-               * @returns {string} The string with unknown domains redacted
+               * Sanitizes content for safe output in GitHub Actions
+               * @param {string} content - The content to sanitize
+               * @returns {string} The sanitized content
                */
-              function sanitizeUrlDomains(s) {
-                return s.replace(/\bhttps:\/\/([^\/\s\])}'"<>&\x00-\x1f]+)/gi, (match, domain) => {
-                  // Extract the hostname part (before first slash, colon, or other delimiter)
-                  const hostname = domain.split(/[\/:\?#]/)[0].toLowerCase();
-                  // Check if this domain or any parent domain is in the allowlist
-                  const isAllowed = allowedDomains.some(allowedDomain => {
-                    const normalizedAllowed = allowedDomain.toLowerCase();
-                    return hostname === normalizedAllowed || hostname.endsWith('.' + normalizedAllowed);
+              function sanitizeContent(content) {
+                if (!content || typeof content !== 'string') {
+                  return '';
+                }
+                // Read allowed domains from environment variable
+                const allowedDomainsEnv = process.env.GITHUB_AW_ALLOWED_DOMAINS;
+                const defaultAllowedDomains = [
+                  'github.com',
+                  'github.io',
+                  'githubusercontent.com',
+                  'githubassets.com',
+                  'github.dev',
+                  'codespaces.new'
+                ];
+                const allowedDomains = allowedDomainsEnv
+                  ? allowedDomainsEnv.split(',').map(d => d.trim()).filter(d => d)
+                  : defaultAllowedDomains;
+                let sanitized = content;
+                // Neutralize @mentions to prevent unintended notifications
+                sanitized = neutralizeMentions(sanitized);
+                // Remove control characters (except newlines and tabs)
+                sanitized = sanitized.replace(/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/g, '');
+                // XML character escaping
+                sanitized = sanitized
+                  .replace(/&/g, '&amp;')   // Must be first to avoid double-escaping
+                  .replace(/</g, '&lt;')
+                  .replace(/>/g, '&gt;')
+                  .replace(/"/g, '&quot;')
+                  .replace(/'/g, '&apos;');
+                // URI filtering - replace non-https protocols with "(redacted)"
+                sanitized = sanitizeUrlProtocols(sanitized);
+                // Domain filtering for HTTPS URIs
+                sanitized = sanitizeUrlDomains(sanitized);
+                // Limit total length to prevent DoS (0.5MB max)
+                const maxLength = 524288;
+                if (sanitized.length > maxLength) {
+                  sanitized = sanitized.substring(0, maxLength) + '\n[Content truncated due to length]';
+                }
+                // Limit number of lines to prevent log flooding (65k max)
+                const lines = sanitized.split('\n');
+                const maxLines = 65000;
+                if (lines.length > maxLines) {
+                  sanitized = lines.slice(0, maxLines).join('\n') + '\n[Content truncated due to line count]';
+                }
+                // Remove ANSI escape sequences
+                sanitized = sanitized.replace(/\x1b\[[0-9;]*[mGKH]/g, '');
+                // Neutralize common bot trigger phrases
+                sanitized = neutralizeBotTriggers(sanitized);
+                // Trim excessive whitespace
+                return sanitized.trim();
+                /**
+                 * Remove unknown domains
+                 * @param {string} s - The string to process
+                 * @returns {string} The string with unknown domains redacted
+                 */
+                function sanitizeUrlDomains(s) {
+                  return s.replace(/\bhttps:\/\/([^\/\s\])}'"<>&\x00-\x1f]+)/gi, (match, domain) => {
+                    // Extract the hostname part (before first slash, colon, or other delimiter)
+                    const hostname = domain.split(/[\/:\?#]/)[0].toLowerCase();
+                    // Check if this domain or any parent domain is in the allowlist
+                    const isAllowed = allowedDomains.some(allowedDomain => {
+                      const normalizedAllowed = allowedDomain.toLowerCase();
+                      return hostname === normalizedAllowed || hostname.endsWith('.' + normalizedAllowed);
+                    });
+                    return isAllowed ? match : '(redacted)';
                   });
-                  return isAllowed ? match : '(redacted)';
-                });
-              }
-              /**
-               * Remove unknown protocols except https
-               * @param {string} s - The string to process
-               * @returns {string} The string with non-https protocols redacted
-               */
-              function sanitizeUrlProtocols(s) {
-                // Match both protocol:// and protocol: patterns
-                return s.replace(/\b(\w+):(?:\/\/)?[^\s\])}'"<>&\x00-\x1f]+/gi, (match, protocol) => {
-                  // Allow https (case insensitive), redact everything else
-                  return protocol.toLowerCase() === 'https' ? match : '(redacted)';
-                });
-              }
-              /**
-               * Neutralizes @mentions by wrapping them in backticks
-               * @param {string} s - The string to process
-               * @returns {string} The string with neutralized mentions
-               */
-              function neutralizeMentions(s) {
-                // Replace @name or @org/team outside code with `@name`
-                return s.replace(/(^|[^\w`])@([A-Za-z0-9](?:[A-Za-z0-9-]{0,37}[A-Za-z0-9])?(?:\/[A-Za-z0-9._-]+)?)/g,
-                  (_m, p1, p2) => `${p1}\`@${p2}\``);
+                }
+                /**
+                 * Remove unknown protocols except https
+                 * @param {string} s - The string to process
+                 * @returns {string} The string with non-https protocols redacted
+                 */
+                function sanitizeUrlProtocols(s) {
+                  // Match both protocol:// and protocol: patterns
+                  return s.replace(/\b(\w+):(?:\/\/)?[^\s\])}'"<>&\x00-\x1f]+/gi, (match, protocol) => {
+                    // Allow https (case insensitive), redact everything else
+                    return protocol.toLowerCase() === 'https' ? match : '(redacted)';
+                  });
+                }
+                /**
+                 * Neutralizes @mentions by wrapping them in backticks
+                 * @param {string} s - The string to process
+                 * @returns {string} The string with neutralized mentions
+                 */
+                function neutralizeMentions(s) {
+                  // Replace @name or @org/team outside code with `@name`
+                  return s.replace(/(^|[^\w`])@([A-Za-z0-9](?:[A-Za-z0-9-]{0,37}[A-Za-z0-9])?(?:\/[A-Za-z0-9._-]+)?)/g,
+                    (_m, p1, p2) => `${p1}\`@${p2}\``);
+                }
+                /**
+                 * Neutralizes bot trigger phrases by wrapping them in backticks
+                 * @param {string} s - The string to process
+                 * @returns {string} The string with neutralized bot triggers
+                 */
+                function neutralizeBotTriggers(s) {
+                  // Neutralize common bot trigger phrases like "fixes #123", "closes #asdfs", etc.
+                  return s.replace(/\b(fixes?|closes?|resolves?|fix|close|resolve)\s+#(\w+)/gi,
+                    (match, action, ref) => `\`${action} #${ref}\``);
+                }
               }
               /**
-               * Neutralizes bot trigger phrases by wrapping them in backticks
-               * @param {string} s - The string to process
-               * @returns {string} The string with neutralized bot triggers
+               * Gets the maximum allowed count for a given output type
+               * @param {string} itemType - The output item type
+               * @param {Object} config - The safe-outputs configuration
+               * @returns {number} The maximum allowed count
                */
-              function neutralizeBotTriggers(s) {
-                // Neutralize common bot trigger phrases like "fixes #123", "closes #asdfs", etc.
-                return s.replace(/\b(fixes?|closes?|resolves?|fix|close|resolve)\s+#(\w+)/gi,
-                  (match, action, ref) => `\`${action} #${ref}\``);
+              function getMaxAllowedForType(itemType, config) {
+                // Check if max is explicitly specified in config
+                if (config && config[itemType] && typeof config[itemType] === 'object' && config[itemType].max) {
+                  return config[itemType].max;
+                }
+                // Use default limits for plural-supported types
+                switch (itemType) {
+                  case 'create-issue':
+                    return 10; // Allow multiple issues
+                  case 'add-issue-comment':
+                    return 10; // Allow multiple comments
+                  case 'create-pull-request':
+                    return 1;  // Only one pull request allowed
+                  case 'add-issue-labels':
+                    return 1;  // Only one labels operation allowed
+                  default:
+                    return 1;  // Default to single item for unknown types
+                }
               }
-            }
-            async function main() {
-              const fs = require("fs");
               const outputFile = process.env.GITHUB_AW_SAFE_OUTPUTS;
               const safeOutputsConfig = process.env.GITHUB_AW_SAFE_OUTPUTS_CONFIG;
               if (!outputFile) {
@@ -422,10 +447,11 @@ jobs:
                     errors.push(`Line ${i + 1}: Unexpected output type '${itemType}'. Expected one of: ${Object.keys(expectedOutputTypes).join(', ')}`);
                     continue;
                   }
-                  // Check for duplicates based on type (since current format allows only one of each)
-                  const existingItem = parsedItems.find(existing => existing.type === itemType);
-                  if (existingItem) {
-                    errors.push(`Line ${i + 1}: Duplicate output type '${itemType}'. Only one item of each type is currently allowed.`);
+                  // Check for too many items of the same type
+                  const typeCount = parsedItems.filter(existing => existing.type === itemType).length;
+                  const maxAllowed = getMaxAllowedForType(itemType, expectedOutputTypes);
+                  if (typeCount >= maxAllowed) {
+                    errors.push(`Line ${i + 1}: Too many items of type '${itemType}'. Maximum allowed: ${maxAllowed}.`);
                     continue;
                   }
                   // Basic validation based on type
diff --git a/.github/workflows/test-claude-mcp.lock.yml b/.github/workflows/test-claude-mcp.lock.yml
index c71bafb03f9..83dc0737eee 100644
--- a/.github/workflows/test-claude-mcp.lock.yml
+++ b/.github/workflows/test-claude-mcp.lock.yml
@@ -450,113 +450,138 @@ jobs:
           GITHUB_AW_SAFE_OUTPUTS_CONFIG: "{\"create-issue\":true}"
         with:
           script: |
-            /**
-             * Sanitizes content for safe output in GitHub Actions
-             * @param {string} content - The content to sanitize
-             * @returns {string} The sanitized content
-             */
-            function sanitizeContent(content) {
-              if (!content || typeof content !== 'string') {
-                return '';
-              }
-              // Read allowed domains from environment variable
-              const allowedDomainsEnv = process.env.GITHUB_AW_ALLOWED_DOMAINS;
-              const defaultAllowedDomains = [
-                'github.com',
-                'github.io',
-                'githubusercontent.com',
-                'githubassets.com',
-                'github.dev',
-                'codespaces.new'
-              ];
-              const allowedDomains = allowedDomainsEnv
-                ? allowedDomainsEnv.split(',').map(d => d.trim()).filter(d => d)
-                : defaultAllowedDomains;
-              let sanitized = content;
-              // Neutralize @mentions to prevent unintended notifications
-              sanitized = neutralizeMentions(sanitized);
-              // Remove control characters (except newlines and tabs)
-              sanitized = sanitized.replace(/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/g, '');
-              // XML character escaping
-              sanitized = sanitized
-                .replace(/&/g, '&amp;')   // Must be first to avoid double-escaping
-                .replace(/</g, '&lt;')
-                .replace(/>/g, '&gt;')
-                .replace(/"/g, '&quot;')
-                .replace(/'/g, '&apos;');
-              // URI filtering - replace non-https protocols with "(redacted)"
-              sanitized = sanitizeUrlProtocols(sanitized);
-              // Domain filtering for HTTPS URIs
-              sanitized = sanitizeUrlDomains(sanitized);
-              // Limit total length to prevent DoS (0.5MB max)
-              const maxLength = 524288;
-              if (sanitized.length > maxLength) {
-                sanitized = sanitized.substring(0, maxLength) + '\n[Content truncated due to length]';
-              }
-              // Limit number of lines to prevent log flooding (65k max)
-              const lines = sanitized.split('\n');
-              const maxLines = 65000;
-              if (lines.length > maxLines) {
-                sanitized = lines.slice(0, maxLines).join('\n') + '\n[Content truncated due to line count]';
-              }
-              // Remove ANSI escape sequences
-              sanitized = sanitized.replace(/\x1b\[[0-9;]*[mGKH]/g, '');
-              // Neutralize common bot trigger phrases
-              sanitized = neutralizeBotTriggers(sanitized);
-              // Trim excessive whitespace
-              return sanitized.trim();
+            async function main() {
+              const fs = require("fs");
               /**
-               * Remove unknown domains
-               * @param {string} s - The string to process
-               * @returns {string} The string with unknown domains redacted
+               * Sanitizes content for safe output in GitHub Actions
+               * @param {string} content - The content to sanitize
+               * @returns {string} The sanitized content
                */
-              function sanitizeUrlDomains(s) {
-                return s.replace(/\bhttps:\/\/([^\/\s\])}'"<>&\x00-\x1f]+)/gi, (match, domain) => {
-                  // Extract the hostname part (before first slash, colon, or other delimiter)
-                  const hostname = domain.split(/[\/:\?#]/)[0].toLowerCase();
-                  // Check if this domain or any parent domain is in the allowlist
-                  const isAllowed = allowedDomains.some(allowedDomain => {
-                    const normalizedAllowed = allowedDomain.toLowerCase();
-                    return hostname === normalizedAllowed || hostname.endsWith('.' + normalizedAllowed);
+              function sanitizeContent(content) {
+                if (!content || typeof content !== 'string') {
+                  return '';
+                }
+                // Read allowed domains from environment variable
+                const allowedDomainsEnv = process.env.GITHUB_AW_ALLOWED_DOMAINS;
+                const defaultAllowedDomains = [
+                  'github.com',
+                  'github.io',
+                  'githubusercontent.com',
+                  'githubassets.com',
+                  'github.dev',
+                  'codespaces.new'
+                ];
+                const allowedDomains = allowedDomainsEnv
+                  ? allowedDomainsEnv.split(',').map(d => d.trim()).filter(d => d)
+                  : defaultAllowedDomains;
+                let sanitized = content;
+                // Neutralize @mentions to prevent unintended notifications
+                sanitized = neutralizeMentions(sanitized);
+                // Remove control characters (except newlines and tabs)
+                sanitized = sanitized.replace(/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/g, '');
+                // XML character escaping
+                sanitized = sanitized
+                  .replace(/&/g, '&amp;')   // Must be first to avoid double-escaping
+                  .replace(/</g, '&lt;')
+                  .replace(/>/g, '&gt;')
+                  .replace(/"/g, '&quot;')
+                  .replace(/'/g, '&apos;');
+                // URI filtering - replace non-https protocols with "(redacted)"
+                sanitized = sanitizeUrlProtocols(sanitized);
+                // Domain filtering for HTTPS URIs
+                sanitized = sanitizeUrlDomains(sanitized);
+                // Limit total length to prevent DoS (0.5MB max)
+                const maxLength = 524288;
+                if (sanitized.length > maxLength) {
+                  sanitized = sanitized.substring(0, maxLength) + '\n[Content truncated due to length]';
+                }
+                // Limit number of lines to prevent log flooding (65k max)
+                const lines = sanitized.split('\n');
+                const maxLines = 65000;
+                if (lines.length > maxLines) {
+                  sanitized = lines.slice(0, maxLines).join('\n') + '\n[Content truncated due to line count]';
+                }
+                // Remove ANSI escape sequences
+                sanitized = sanitized.replace(/\x1b\[[0-9;]*[mGKH]/g, '');
+                // Neutralize common bot trigger phrases
+                sanitized = neutralizeBotTriggers(sanitized);
+                // Trim excessive whitespace
+                return sanitized.trim();
+                /**
+                 * Remove unknown domains
+                 * @param {string} s - The string to process
+                 * @returns {string} The string with unknown domains redacted
+                 */
+                function sanitizeUrlDomains(s) {
+                  return s.replace(/\bhttps:\/\/([^\/\s\])}'"<>&\x00-\x1f]+)/gi, (match, domain) => {
+                    // Extract the hostname part (before first slash, colon, or other delimiter)
+                    const hostname = domain.split(/[\/:\?#]/)[0].toLowerCase();
+                    // Check if this domain or any parent domain is in the allowlist
+                    const isAllowed = allowedDomains.some(allowedDomain => {
+                      const normalizedAllowed = allowedDomain.toLowerCase();
+                      return hostname === normalizedAllowed || hostname.endsWith('.' + normalizedAllowed);
+                    });
+                    return isAllowed ? match : '(redacted)';
                   });
-                  return isAllowed ? match : '(redacted)';
-                });
-              }
-              /**
-               * Remove unknown protocols except https
-               * @param {string} s - The string to process
-               * @returns {string} The string with non-https protocols redacted
-               */
-              function sanitizeUrlProtocols(s) {
-                // Match both protocol:// and protocol: patterns
-                return s.replace(/\b(\w+):(?:\/\/)?[^\s\])}'"<>&\x00-\x1f]+/gi, (match, protocol) => {
-                  // Allow https (case insensitive), redact everything else
-                  return protocol.toLowerCase() === 'https' ? match : '(redacted)';
-                });
-              }
-              /**
-               * Neutralizes @mentions by wrapping them in backticks
-               * @param {string} s - The string to process
-               * @returns {string} The string with neutralized mentions
-               */
-              function neutralizeMentions(s) {
-                // Replace @name or @org/team outside code with `@name`
-                return s.replace(/(^|[^\w`])@([A-Za-z0-9](?:[A-Za-z0-9-]{0,37}[A-Za-z0-9])?(?:\/[A-Za-z0-9._-]+)?)/g,
-                  (_m, p1, p2) => `${p1}\`@${p2}\``);
+                }
+                /**
+                 * Remove unknown protocols except https
+                 * @param {string} s - The string to process
+                 * @returns {string} The string with non-https protocols redacted
+                 */
+                function sanitizeUrlProtocols(s) {
+                  // Match both protocol:// and protocol: patterns
+                  return s.replace(/\b(\w+):(?:\/\/)?[^\s\])}'"<>&\x00-\x1f]+/gi, (match, protocol) => {
+                    // Allow https (case insensitive), redact everything else
+                    return protocol.toLowerCase() === 'https' ? match : '(redacted)';
+                  });
+                }
+                /**
+                 * Neutralizes @mentions by wrapping them in backticks
+                 * @param {string} s - The string to process
+                 * @returns {string} The string with neutralized mentions
+                 */
+                function neutralizeMentions(s) {
+                  // Replace @name or @org/team outside code with `@name`
+                  return s.replace(/(^|[^\w`])@([A-Za-z0-9](?:[A-Za-z0-9-]{0,37}[A-Za-z0-9])?(?:\/[A-Za-z0-9._-]+)?)/g,
+                    (_m, p1, p2) => `${p1}\`@${p2}\``);
+                }
+                /**
+                 * Neutralizes bot trigger phrases by wrapping them in backticks
+                 * @param {string} s - The string to process
+                 * @returns {string} The string with neutralized bot triggers
+                 */
+                function neutralizeBotTriggers(s) {
+                  // Neutralize common bot trigger phrases like "fixes #123", "closes #asdfs", etc.
+                  return s.replace(/\b(fixes?|closes?|resolves?|fix|close|resolve)\s+#(\w+)/gi,
+                    (match, action, ref) => `\`${action} #${ref}\``);
+                }
               }
               /**
-               * Neutralizes bot trigger phrases by wrapping them in backticks
-               * @param {string} s - The string to process
-               * @returns {string} The string with neutralized bot triggers
+               * Gets the maximum allowed count for a given output type
+               * @param {string} itemType - The output item type
+               * @param {Object} config - The safe-outputs configuration
+               * @returns {number} The maximum allowed count
                */
-              function neutralizeBotTriggers(s) {
-                // Neutralize common bot trigger phrases like "fixes #123", "closes #asdfs", etc.
-                return s.replace(/\b(fixes?|closes?|resolves?|fix|close|resolve)\s+#(\w+)/gi,
-                  (match, action, ref) => `\`${action} #${ref}\``);
+              function getMaxAllowedForType(itemType, config) {
+                // Check if max is explicitly specified in config
+                if (config && config[itemType] && typeof config[itemType] === 'object' && config[itemType].max) {
+                  return config[itemType].max;
+                }
+                // Use default limits for plural-supported types
+                switch (itemType) {
+                  case 'create-issue':
+                    return 10; // Allow multiple issues
+                  case 'add-issue-comment':
+                    return 10; // Allow multiple comments
+                  case 'create-pull-request':
+                    return 1;  // Only one pull request allowed
+                  case 'add-issue-labels':
+                    return 1;  // Only one labels operation allowed
+                  default:
+                    return 1;  // Default to single item for unknown types
+                }
               }
-            }
-            async function main() {
-              const fs = require("fs");
               const outputFile = process.env.GITHUB_AW_SAFE_OUTPUTS;
               const safeOutputsConfig = process.env.GITHUB_AW_SAFE_OUTPUTS_CONFIG;
               if (!outputFile) {
@@ -606,10 +631,11 @@ jobs:
                     errors.push(`Line ${i + 1}: Unexpected output type '${itemType}'. Expected one of: ${Object.keys(expectedOutputTypes).join(', ')}`);
                     continue;
                   }
-                  // Check for duplicates based on type (since current format allows only one of each)
-                  const existingItem = parsedItems.find(existing => existing.type === itemType);
-                  if (existingItem) {
-                    errors.push(`Line ${i + 1}: Duplicate output type '${itemType}'. Only one item of each type is currently allowed.`);
+                  // Check for too many items of the same type
+                  const typeCount = parsedItems.filter(existing => existing.type === itemType).length;
+                  const maxAllowed = getMaxAllowedForType(itemType, expectedOutputTypes);
+                  if (typeCount >= maxAllowed) {
+                    errors.push(`Line ${i + 1}: Too many items of type '${itemType}'. Maximum allowed: ${maxAllowed}.`);
                     continue;
                   }
                   // Basic validation based on type
@@ -1109,83 +1135,101 @@ jobs:
                 console.log('No valid items found in agent output');
                 return;
               }
-              // Find the create-issue item
-              const createIssueItem = validatedOutput.items.find(/** @param {any} item */ item => item.type === 'create-issue');
-              if (!createIssueItem) {
-                console.log('No create-issue item found in agent output');
+              // Find all create-issue items
+              const createIssueItems = validatedOutput.items.filter(/** @param {any} item */ item => item.type === 'create-issue');
+              if (createIssueItems.length === 0) {
+                console.log('No create-issue items found in agent output');
                 return;
               }
-              console.log('Found create-issue item:', { title: createIssueItem.title, bodyLength: createIssueItem.body.length });
+              console.log(`Found ${createIssueItems.length} create-issue item(s)`);
               // Check if we're in an issue context (triggered by an issue event)
               const parentIssueNumber = context.payload?.issue?.number;
-              // Parse labels from environment variable (comma-separated string) or from the JSON item
+              // Parse labels from environment variable (comma-separated string)
               const labelsEnv = process.env.GITHUB_AW_ISSUE_LABELS;
-              let labels = labelsEnv ? labelsEnv.split(',').map(/** @param {string} label */ label => label.trim()).filter(/** @param {string} label */ label => label) : [];
-              // If the item has labels, use those instead (or merge them)
-              if (createIssueItem.labels && Array.isArray(createIssueItem.labels)) {
-                labels = [...labels, ...createIssueItem.labels].filter(Boolean);
-              }
-              // Extract title and body from the JSON item
-              let title = createIssueItem.title ? createIssueItem.title.trim() : '';
-              let bodyLines = createIssueItem.body.split('\n');
-              // If no title was found, use the body content as title (or a default)
-              if (!title) {
-                title = createIssueItem.body || 'Agent Output';
-              }
-              // Apply title prefix if provided via environment variable
-              const titlePrefix = process.env.GITHUB_AW_ISSUE_TITLE_PREFIX;
-              if (titlePrefix && !title.startsWith(titlePrefix)) {
-                title = titlePrefix + title;
-              }
-              if (parentIssueNumber) {
-                console.log('Detected issue context, parent issue #' + parentIssueNumber);
-                // Add reference to parent issue in the child issue body
-                bodyLines.push(`Related to #${parentIssueNumber}`);
-              }
-              // Add AI disclaimer with run id, run htmlurl
-              // Add AI disclaimer with workflow run information
-              const runId = context.runId;
-              const runUrl = context.payload.repository 
-                ? `${context.payload.repository.html_url}/actions/runs/${runId}`
-                : `https://github.com/actions/runs/${runId}`;  
-              bodyLines.push(``, ``, `> Generated by Agentic Workflow Run [${runId}](${runUrl})`, '');
-              // Prepare the body content
-              const body = bodyLines.join('\n').trim();
-              console.log('Creating issue with title:', title);
-              console.log('Labels:', labels);
-              console.log('Body length:', body.length);
-              // Create the issue using GitHub API
-              const { data: issue } = await github.rest.issues.create({
-                owner: context.repo.owner,
-                repo: context.repo.repo,
-                title: title,
-                body: body,
-                labels: labels
-              });
-              console.log('Created issue #' + issue.number + ': ' + issue.html_url);
-              // If we have a parent issue, add a comment to it referencing the new child issue
-              if (parentIssueNumber) {
+              let envLabels = labelsEnv ? labelsEnv.split(',').map(/** @param {string} label */ label => label.trim()).filter(/** @param {string} label */ label => label) : [];
+              const createdIssues = [];
+              // Process each create-issue item
+              for (let i = 0; i < createIssueItems.length; i++) {
+                const createIssueItem = createIssueItems[i];
+                console.log(`Processing create-issue item ${i + 1}/${createIssueItems.length}:`, { title: createIssueItem.title, bodyLength: createIssueItem.body.length });
+                // Merge environment labels with item-specific labels
+                let labels = [...envLabels];
+                if (createIssueItem.labels && Array.isArray(createIssueItem.labels)) {
+                  labels = [...labels, ...createIssueItem.labels].filter(Boolean);
+                }
+                // Extract title and body from the JSON item
+                let title = createIssueItem.title ? createIssueItem.title.trim() : '';
+                let bodyLines = createIssueItem.body.split('\n');
+                // If no title was found, use the body content as title (or a default)
+                if (!title) {
+                  title = createIssueItem.body || 'Agent Output';
+                }
+                // Apply title prefix if provided via environment variable
+                const titlePrefix = process.env.GITHUB_AW_ISSUE_TITLE_PREFIX;
+                if (titlePrefix && !title.startsWith(titlePrefix)) {
+                  title = titlePrefix + title;
+                }
+                if (parentIssueNumber) {
+                  console.log('Detected issue context, parent issue #' + parentIssueNumber);
+                  // Add reference to parent issue in the child issue body
+                  bodyLines.push(`Related to #${parentIssueNumber}`);
+                }
+                // Add AI disclaimer with run id, run htmlurl
+                // Add AI disclaimer with workflow run information
+                const runId = context.runId;
+                const runUrl = context.payload.repository 
+                  ? `${context.payload.repository.html_url}/actions/runs/${runId}`
+                  : `https://github.com/actions/runs/${runId}`;  
+                bodyLines.push(``, ``, `> Generated by Agentic Workflow Run [${runId}](${runUrl})`, '');
+                // Prepare the body content
+                const body = bodyLines.join('\n').trim();
+                console.log('Creating issue with title:', title);
+                console.log('Labels:', labels);
+                console.log('Body length:', body.length);
                 try {
-                  await github.rest.issues.createComment({
+                  // Create the issue using GitHub API
+                  const { data: issue } = await github.rest.issues.create({
                     owner: context.repo.owner,
                     repo: context.repo.repo,
-                    issue_number: parentIssueNumber,
-                    body: `Created related issue: #${issue.number}`
+                    title: title,
+                    body: body,
+                    labels: labels
                   });
-                  console.log('Added comment to parent issue #' + parentIssueNumber);
+                  console.log('Created issue #' + issue.number + ': ' + issue.html_url);
+                  createdIssues.push(issue);
+                  // If we have a parent issue, add a comment to it referencing the new child issue
+                  if (parentIssueNumber) {
+                    try {
+                      await github.rest.issues.createComment({
+                        owner: context.repo.owner,
+                        repo: context.repo.repo,
+                        issue_number: parentIssueNumber,
+                        body: `Created related issue: #${issue.number}`
+                      });
+                      console.log('Added comment to parent issue #' + parentIssueNumber);
+                    } catch (error) {
+                      console.log('Warning: Could not add comment to parent issue:', error instanceof Error ? error.message : String(error));
+                    }
+                  }
+                  // Set output for the last created issue (for backward compatibility)
+                  if (i === createIssueItems.length - 1) {
+                    core.setOutput('issue_number', issue.number);
+                    core.setOutput('issue_url', issue.html_url);
+                  }
                 } catch (error) {
-                  console.log('Warning: Could not add comment to parent issue:', error instanceof Error ? error.message : String(error));
+                  console.error(`✗ Failed to create issue "${title}":`, error instanceof Error ? error.message : String(error));
+                  throw error;
+                }
+              }
+              // Write summary for all created issues
+              if (createdIssues.length > 0) {
+                let summaryContent = '\n\n## GitHub Issues\n';
+                for (const issue of createdIssues) {
+                  summaryContent += `- Issue #${issue.number}: [${issue.title}](${issue.html_url})\n`;
                 }
+                await core.summary.addRaw(summaryContent).write();
               }
-              // Set output for other jobs to use
-              core.setOutput('issue_number', issue.number);
-              core.setOutput('issue_url', issue.html_url);
-              // write issue to summary
-              await core.summary.addRaw(`
-            ## GitHub Issue
-            - Issue ID: ${issue.number}
-            - Issue URL: ${issue.html_url}
-            `).write();
+              console.log(`Successfully created ${createdIssues.length} issue(s)`);
             }
             await main();
 
diff --git a/.github/workflows/test-codex-add-issue-comment.lock.yml b/.github/workflows/test-codex-add-issue-comment.lock.yml
index e38d150af83..e27ab812d28 100644
--- a/.github/workflows/test-codex-add-issue-comment.lock.yml
+++ b/.github/workflows/test-codex-add-issue-comment.lock.yml
@@ -366,113 +366,138 @@ jobs:
           GITHUB_AW_SAFE_OUTPUTS_CONFIG: "{\"add-issue-comment\":true}"
         with:
           script: |
-            /**
-             * Sanitizes content for safe output in GitHub Actions
-             * @param {string} content - The content to sanitize
-             * @returns {string} The sanitized content
-             */
-            function sanitizeContent(content) {
-              if (!content || typeof content !== 'string') {
-                return '';
-              }
-              // Read allowed domains from environment variable
-              const allowedDomainsEnv = process.env.GITHUB_AW_ALLOWED_DOMAINS;
-              const defaultAllowedDomains = [
-                'github.com',
-                'github.io',
-                'githubusercontent.com',
-                'githubassets.com',
-                'github.dev',
-                'codespaces.new'
-              ];
-              const allowedDomains = allowedDomainsEnv
-                ? allowedDomainsEnv.split(',').map(d => d.trim()).filter(d => d)
-                : defaultAllowedDomains;
-              let sanitized = content;
-              // Neutralize @mentions to prevent unintended notifications
-              sanitized = neutralizeMentions(sanitized);
-              // Remove control characters (except newlines and tabs)
-              sanitized = sanitized.replace(/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/g, '');
-              // XML character escaping
-              sanitized = sanitized
-                .replace(/&/g, '&amp;')   // Must be first to avoid double-escaping
-                .replace(/</g, '&lt;')
-                .replace(/>/g, '&gt;')
-                .replace(/"/g, '&quot;')
-                .replace(/'/g, '&apos;');
-              // URI filtering - replace non-https protocols with "(redacted)"
-              sanitized = sanitizeUrlProtocols(sanitized);
-              // Domain filtering for HTTPS URIs
-              sanitized = sanitizeUrlDomains(sanitized);
-              // Limit total length to prevent DoS (0.5MB max)
-              const maxLength = 524288;
-              if (sanitized.length > maxLength) {
-                sanitized = sanitized.substring(0, maxLength) + '\n[Content truncated due to length]';
-              }
-              // Limit number of lines to prevent log flooding (65k max)
-              const lines = sanitized.split('\n');
-              const maxLines = 65000;
-              if (lines.length > maxLines) {
-                sanitized = lines.slice(0, maxLines).join('\n') + '\n[Content truncated due to line count]';
-              }
-              // Remove ANSI escape sequences
-              sanitized = sanitized.replace(/\x1b\[[0-9;]*[mGKH]/g, '');
-              // Neutralize common bot trigger phrases
-              sanitized = neutralizeBotTriggers(sanitized);
-              // Trim excessive whitespace
-              return sanitized.trim();
+            async function main() {
+              const fs = require("fs");
               /**
-               * Remove unknown domains
-               * @param {string} s - The string to process
-               * @returns {string} The string with unknown domains redacted
+               * Sanitizes content for safe output in GitHub Actions
+               * @param {string} content - The content to sanitize
+               * @returns {string} The sanitized content
                */
-              function sanitizeUrlDomains(s) {
-                return s.replace(/\bhttps:\/\/([^\/\s\])}'"<>&\x00-\x1f]+)/gi, (match, domain) => {
-                  // Extract the hostname part (before first slash, colon, or other delimiter)
-                  const hostname = domain.split(/[\/:\?#]/)[0].toLowerCase();
-                  // Check if this domain or any parent domain is in the allowlist
-                  const isAllowed = allowedDomains.some(allowedDomain => {
-                    const normalizedAllowed = allowedDomain.toLowerCase();
-                    return hostname === normalizedAllowed || hostname.endsWith('.' + normalizedAllowed);
+              function sanitizeContent(content) {
+                if (!content || typeof content !== 'string') {
+                  return '';
+                }
+                // Read allowed domains from environment variable
+                const allowedDomainsEnv = process.env.GITHUB_AW_ALLOWED_DOMAINS;
+                const defaultAllowedDomains = [
+                  'github.com',
+                  'github.io',
+                  'githubusercontent.com',
+                  'githubassets.com',
+                  'github.dev',
+                  'codespaces.new'
+                ];
+                const allowedDomains = allowedDomainsEnv
+                  ? allowedDomainsEnv.split(',').map(d => d.trim()).filter(d => d)
+                  : defaultAllowedDomains;
+                let sanitized = content;
+                // Neutralize @mentions to prevent unintended notifications
+                sanitized = neutralizeMentions(sanitized);
+                // Remove control characters (except newlines and tabs)
+                sanitized = sanitized.replace(/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/g, '');
+                // XML character escaping
+                sanitized = sanitized
+                  .replace(/&/g, '&amp;')   // Must be first to avoid double-escaping
+                  .replace(/</g, '&lt;')
+                  .replace(/>/g, '&gt;')
+                  .replace(/"/g, '&quot;')
+                  .replace(/'/g, '&apos;');
+                // URI filtering - replace non-https protocols with "(redacted)"
+                sanitized = sanitizeUrlProtocols(sanitized);
+                // Domain filtering for HTTPS URIs
+                sanitized = sanitizeUrlDomains(sanitized);
+                // Limit total length to prevent DoS (0.5MB max)
+                const maxLength = 524288;
+                if (sanitized.length > maxLength) {
+                  sanitized = sanitized.substring(0, maxLength) + '\n[Content truncated due to length]';
+                }
+                // Limit number of lines to prevent log flooding (65k max)
+                const lines = sanitized.split('\n');
+                const maxLines = 65000;
+                if (lines.length > maxLines) {
+                  sanitized = lines.slice(0, maxLines).join('\n') + '\n[Content truncated due to line count]';
+                }
+                // Remove ANSI escape sequences
+                sanitized = sanitized.replace(/\x1b\[[0-9;]*[mGKH]/g, '');
+                // Neutralize common bot trigger phrases
+                sanitized = neutralizeBotTriggers(sanitized);
+                // Trim excessive whitespace
+                return sanitized.trim();
+                /**
+                 * Remove unknown domains
+                 * @param {string} s - The string to process
+                 * @returns {string} The string with unknown domains redacted
+                 */
+                function sanitizeUrlDomains(s) {
+                  return s.replace(/\bhttps:\/\/([^\/\s\])}'"<>&\x00-\x1f]+)/gi, (match, domain) => {
+                    // Extract the hostname part (before first slash, colon, or other delimiter)
+                    const hostname = domain.split(/[\/:\?#]/)[0].toLowerCase();
+                    // Check if this domain or any parent domain is in the allowlist
+                    const isAllowed = allowedDomains.some(allowedDomain => {
+                      const normalizedAllowed = allowedDomain.toLowerCase();
+                      return hostname === normalizedAllowed || hostname.endsWith('.' + normalizedAllowed);
+                    });
+                    return isAllowed ? match : '(redacted)';
                   });
-                  return isAllowed ? match : '(redacted)';
-                });
-              }
-              /**
-               * Remove unknown protocols except https
-               * @param {string} s - The string to process
-               * @returns {string} The string with non-https protocols redacted
-               */
-              function sanitizeUrlProtocols(s) {
-                // Match both protocol:// and protocol: patterns
-                return s.replace(/\b(\w+):(?:\/\/)?[^\s\])}'"<>&\x00-\x1f]+/gi, (match, protocol) => {
-                  // Allow https (case insensitive), redact everything else
-                  return protocol.toLowerCase() === 'https' ? match : '(redacted)';
-                });
-              }
-              /**
-               * Neutralizes @mentions by wrapping them in backticks
-               * @param {string} s - The string to process
-               * @returns {string} The string with neutralized mentions
-               */
-              function neutralizeMentions(s) {
-                // Replace @name or @org/team outside code with `@name`
-                return s.replace(/(^|[^\w`])@([A-Za-z0-9](?:[A-Za-z0-9-]{0,37}[A-Za-z0-9])?(?:\/[A-Za-z0-9._-]+)?)/g,
-                  (_m, p1, p2) => `${p1}\`@${p2}\``);
+                }
+                /**
+                 * Remove unknown protocols except https
+                 * @param {string} s - The string to process
+                 * @returns {string} The string with non-https protocols redacted
+                 */
+                function sanitizeUrlProtocols(s) {
+                  // Match both protocol:// and protocol: patterns
+                  return s.replace(/\b(\w+):(?:\/\/)?[^\s\])}'"<>&\x00-\x1f]+/gi, (match, protocol) => {
+                    // Allow https (case insensitive), redact everything else
+                    return protocol.toLowerCase() === 'https' ? match : '(redacted)';
+                  });
+                }
+                /**
+                 * Neutralizes @mentions by wrapping them in backticks
+                 * @param {string} s - The string to process
+                 * @returns {string} The string with neutralized mentions
+                 */
+                function neutralizeMentions(s) {
+                  // Replace @name or @org/team outside code with `@name`
+                  return s.replace(/(^|[^\w`])@([A-Za-z0-9](?:[A-Za-z0-9-]{0,37}[A-Za-z0-9])?(?:\/[A-Za-z0-9._-]+)?)/g,
+                    (_m, p1, p2) => `${p1}\`@${p2}\``);
+                }
+                /**
+                 * Neutralizes bot trigger phrases by wrapping them in backticks
+                 * @param {string} s - The string to process
+                 * @returns {string} The string with neutralized bot triggers
+                 */
+                function neutralizeBotTriggers(s) {
+                  // Neutralize common bot trigger phrases like "fixes #123", "closes #asdfs", etc.
+                  return s.replace(/\b(fixes?|closes?|resolves?|fix|close|resolve)\s+#(\w+)/gi,
+                    (match, action, ref) => `\`${action} #${ref}\``);
+                }
               }
               /**
-               * Neutralizes bot trigger phrases by wrapping them in backticks
-               * @param {string} s - The string to process
-               * @returns {string} The string with neutralized bot triggers
+               * Gets the maximum allowed count for a given output type
+               * @param {string} itemType - The output item type
+               * @param {Object} config - The safe-outputs configuration
+               * @returns {number} The maximum allowed count
                */
-              function neutralizeBotTriggers(s) {
-                // Neutralize common bot trigger phrases like "fixes #123", "closes #asdfs", etc.
-                return s.replace(/\b(fixes?|closes?|resolves?|fix|close|resolve)\s+#(\w+)/gi,
-                  (match, action, ref) => `\`${action} #${ref}\``);
+              function getMaxAllowedForType(itemType, config) {
+                // Check if max is explicitly specified in config
+                if (config && config[itemType] && typeof config[itemType] === 'object' && config[itemType].max) {
+                  return config[itemType].max;
+                }
+                // Use default limits for plural-supported types
+                switch (itemType) {
+                  case 'create-issue':
+                    return 10; // Allow multiple issues
+                  case 'add-issue-comment':
+                    return 10; // Allow multiple comments
+                  case 'create-pull-request':
+                    return 1;  // Only one pull request allowed
+                  case 'add-issue-labels':
+                    return 1;  // Only one labels operation allowed
+                  default:
+                    return 1;  // Default to single item for unknown types
+                }
               }
-            }
-            async function main() {
-              const fs = require("fs");
               const outputFile = process.env.GITHUB_AW_SAFE_OUTPUTS;
               const safeOutputsConfig = process.env.GITHUB_AW_SAFE_OUTPUTS_CONFIG;
               if (!outputFile) {
@@ -522,10 +547,11 @@ jobs:
                     errors.push(`Line ${i + 1}: Unexpected output type '${itemType}'. Expected one of: ${Object.keys(expectedOutputTypes).join(', ')}`);
                     continue;
                   }
-                  // Check for duplicates based on type (since current format allows only one of each)
-                  const existingItem = parsedItems.find(existing => existing.type === itemType);
-                  if (existingItem) {
-                    errors.push(`Line ${i + 1}: Duplicate output type '${itemType}'. Only one item of each type is currently allowed.`);
+                  // Check for too many items of the same type
+                  const typeCount = parsedItems.filter(existing => existing.type === itemType).length;
+                  const maxAllowed = getMaxAllowedForType(itemType, expectedOutputTypes);
+                  if (typeCount >= maxAllowed) {
+                    errors.push(`Line ${i + 1}: Too many items of type '${itemType}'. Maximum allowed: ${maxAllowed}.`);
                     continue;
                   }
                   // Basic validation based on type
@@ -972,13 +998,13 @@ jobs:
                 console.log('No valid items found in agent output');
                 return;
               }
-              // Find the add-issue-comment item
-              const commentItem = validatedOutput.items.find(/** @param {any} item */ item => item.type === 'add-issue-comment');
-              if (!commentItem) {
-                console.log('No add-issue-comment item found in agent output');
+              // Find all add-issue-comment items
+              const commentItems = validatedOutput.items.filter(/** @param {any} item */ item => item.type === 'add-issue-comment');
+              if (commentItems.length === 0) {
+                console.log('No add-issue-comment items found in agent output');
                 return;
               }
-              console.log('Found add-issue-comment item:', { bodyLength: commentItem.body.length });
+              console.log(`Found ${commentItems.length} add-issue-comment item(s)`);
               // Check if we're in an issue or pull request context
               const isIssueContext = context.eventName === 'issues' || context.eventName === 'issue_comment';
               const isPRContext = context.eventName === 'pull_request' || context.eventName === 'pull_request_review' || context.eventName === 'pull_request_review_comment';
@@ -986,57 +1012,75 @@ jobs:
                 console.log('Not running in issue or pull request context, skipping comment creation');
                 return;
               }
-              // Determine the issue/PR number and comment endpoint
-              let issueNumber;
-              let commentEndpoint;
-              if (isIssueContext) {
-                if (context.payload.issue) {
-                  issueNumber = context.payload.issue.number;
-                  commentEndpoint = 'issues';
-                } else {
-                  console.log('Issue context detected but no issue found in payload');
-                  return;
+              const createdComments = [];
+              // Process each comment item
+              for (let i = 0; i < commentItems.length; i++) {
+                const commentItem = commentItems[i];
+                console.log(`Processing add-issue-comment item ${i + 1}/${commentItems.length}:`, { bodyLength: commentItem.body.length });
+                // Determine the issue/PR number and comment endpoint for this comment
+                let issueNumber;
+                let commentEndpoint;
+                if (isIssueContext) {
+                  if (context.payload.issue) {
+                    issueNumber = context.payload.issue.number;
+                    commentEndpoint = 'issues';
+                  } else {
+                    console.log('Issue context detected but no issue found in payload');
+                    continue;
+                  }
+                } else if (isPRContext) {
+                  if (context.payload.pull_request) {
+                    issueNumber = context.payload.pull_request.number;
+                    commentEndpoint = 'issues'; // PR comments use the issues API endpoint
+                  } else {
+                    console.log('Pull request context detected but no pull request found in payload');
+                    continue;
+                  }
                 }
-              } else if (isPRContext) {
-                if (context.payload.pull_request) {
-                  issueNumber = context.payload.pull_request.number;
-                  commentEndpoint = 'issues'; // PR comments use the issues API endpoint
-                } else {
-                  console.log('Pull request context detected but no pull request found in payload');
-                  return;
+                if (!issueNumber) {
+                  console.log('Could not determine issue or pull request number');
+                  continue;
+                }
+                // Extract body from the JSON item
+                let body = commentItem.body.trim();
+                // Add AI disclaimer with run id, run htmlurl
+                const runId = context.runId;
+                const runUrl = context.payload.repository 
+                  ? `${context.payload.repository.html_url}/actions/runs/${runId}`
+                  : `https://github.com/actions/runs/${runId}`;  
+                body += `\n\n> Generated by Agentic Workflow Run [${runId}](${runUrl})\n`;
+                console.log(`Creating comment on ${commentEndpoint} #${issueNumber}`);
+                console.log('Comment content length:', body.length);
+                try {
+                  // Create the comment using GitHub API
+                  const { data: comment } = await github.rest.issues.createComment({
+                    owner: context.repo.owner,
+                    repo: context.repo.repo,
+                    issue_number: issueNumber,
+                    body: body
+                  });
+                  console.log('Created comment #' + comment.id + ': ' + comment.html_url);
+                  createdComments.push(comment);
+                  // Set output for the last created comment (for backward compatibility)
+                  if (i === commentItems.length - 1) {
+                    core.setOutput('comment_id', comment.id);
+                    core.setOutput('comment_url', comment.html_url);
+                  }
+                } catch (error) {
+                  console.error(`✗ Failed to create comment:`, error instanceof Error ? error.message : String(error));
+                  throw error;
                 }
               }
-              if (!issueNumber) {
-                console.log('Could not determine issue or pull request number');
-                return;
+              // Write summary for all created comments
+              if (createdComments.length > 0) {
+                let summaryContent = '\n\n## GitHub Comments\n';
+                for (const comment of createdComments) {
+                  summaryContent += `- Comment #${comment.id}: [View Comment](${comment.html_url})\n`;
+                }
+                await core.summary.addRaw(summaryContent).write();
               }
-              // Extract body from the JSON item
-              let body = commentItem.body.trim();
-              // Add AI disclaimer with run id, run htmlurl
-              const runId = context.runId;
-              const runUrl = context.payload.repository 
-                ? `${context.payload.repository.html_url}/actions/runs/${runId}`
-                : `https://github.com/actions/runs/${runId}`;  
-              body += `\n\n> Generated by Agentic Workflow Run [${runId}](${runUrl})\n`;
-              console.log(`Creating comment on ${commentEndpoint} #${issueNumber}`);
-              console.log('Comment content length:', body.length);
-              // Create the comment using GitHub API
-              const { data: comment } = await github.rest.issues.createComment({
-                owner: context.repo.owner,
-                repo: context.repo.repo,
-                issue_number: issueNumber,
-                body: body
-              });
-              console.log('Created comment #' + comment.id + ': ' + comment.html_url);
-              // Set output for other jobs to use
-              core.setOutput('comment_id', comment.id);
-              core.setOutput('comment_url', comment.html_url);
-              // write comment id, url to the github_step_summary
-              await core.summary.addRaw(`
-            ## GitHub Comment
-            - Comment ID: ${comment.id}
-            - Comment URL: ${comment.html_url}
-            `).write();
+              console.log(`Successfully created ${createdComments.length} comment(s)`);
+              return createdComments;
             }
             await main();
 
diff --git a/.github/workflows/test-codex-add-issue-labels.lock.yml b/.github/workflows/test-codex-add-issue-labels.lock.yml
index 6a0a16b5f79..7213691c7d3 100644
--- a/.github/workflows/test-codex-add-issue-labels.lock.yml
+++ b/.github/workflows/test-codex-add-issue-labels.lock.yml
@@ -366,113 +366,138 @@ jobs:
           GITHUB_AW_SAFE_OUTPUTS_CONFIG: "{\"add-issue-labels\":true}"
         with:
           script: |
-            /**
-             * Sanitizes content for safe output in GitHub Actions
-             * @param {string} content - The content to sanitize
-             * @returns {string} The sanitized content
-             */
-            function sanitizeContent(content) {
-              if (!content || typeof content !== 'string') {
-                return '';
-              }
-              // Read allowed domains from environment variable
-              const allowedDomainsEnv = process.env.GITHUB_AW_ALLOWED_DOMAINS;
-              const defaultAllowedDomains = [
-                'github.com',
-                'github.io',
-                'githubusercontent.com',
-                'githubassets.com',
-                'github.dev',
-                'codespaces.new'
-              ];
-              const allowedDomains = allowedDomainsEnv
-                ? allowedDomainsEnv.split(',').map(d => d.trim()).filter(d => d)
-                : defaultAllowedDomains;
-              let sanitized = content;
-              // Neutralize @mentions to prevent unintended notifications
-              sanitized = neutralizeMentions(sanitized);
-              // Remove control characters (except newlines and tabs)
-              sanitized = sanitized.replace(/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/g, '');
-              // XML character escaping
-              sanitized = sanitized
-                .replace(/&/g, '&amp;')   // Must be first to avoid double-escaping
-                .replace(/</g, '&lt;')
-                .replace(/>/g, '&gt;')
-                .replace(/"/g, '&quot;')
-                .replace(/'/g, '&apos;');
-              // URI filtering - replace non-https protocols with "(redacted)"
-              sanitized = sanitizeUrlProtocols(sanitized);
-              // Domain filtering for HTTPS URIs
-              sanitized = sanitizeUrlDomains(sanitized);
-              // Limit total length to prevent DoS (0.5MB max)
-              const maxLength = 524288;
-              if (sanitized.length > maxLength) {
-                sanitized = sanitized.substring(0, maxLength) + '\n[Content truncated due to length]';
-              }
-              // Limit number of lines to prevent log flooding (65k max)
-              const lines = sanitized.split('\n');
-              const maxLines = 65000;
-              if (lines.length > maxLines) {
-                sanitized = lines.slice(0, maxLines).join('\n') + '\n[Content truncated due to line count]';
-              }
-              // Remove ANSI escape sequences
-              sanitized = sanitized.replace(/\x1b\[[0-9;]*[mGKH]/g, '');
-              // Neutralize common bot trigger phrases
-              sanitized = neutralizeBotTriggers(sanitized);
-              // Trim excessive whitespace
-              return sanitized.trim();
+            async function main() {
+              const fs = require("fs");
               /**
-               * Remove unknown domains
-               * @param {string} s - The string to process
-               * @returns {string} The string with unknown domains redacted
+               * Sanitizes content for safe output in GitHub Actions
+               * @param {string} content - The content to sanitize
+               * @returns {string} The sanitized content
                */
-              function sanitizeUrlDomains(s) {
-                return s.replace(/\bhttps:\/\/([^\/\s\])}'"<>&\x00-\x1f]+)/gi, (match, domain) => {
-                  // Extract the hostname part (before first slash, colon, or other delimiter)
-                  const hostname = domain.split(/[\/:\?#]/)[0].toLowerCase();
-                  // Check if this domain or any parent domain is in the allowlist
-                  const isAllowed = allowedDomains.some(allowedDomain => {
-                    const normalizedAllowed = allowedDomain.toLowerCase();
-                    return hostname === normalizedAllowed || hostname.endsWith('.' + normalizedAllowed);
+              function sanitizeContent(content) {
+                if (!content || typeof content !== 'string') {
+                  return '';
+                }
+                // Read allowed domains from environment variable
+                const allowedDomainsEnv = process.env.GITHUB_AW_ALLOWED_DOMAINS;
+                const defaultAllowedDomains = [
+                  'github.com',
+                  'github.io',
+                  'githubusercontent.com',
+                  'githubassets.com',
+                  'github.dev',
+                  'codespaces.new'
+                ];
+                const allowedDomains = allowedDomainsEnv
+                  ? allowedDomainsEnv.split(',').map(d => d.trim()).filter(d => d)
+                  : defaultAllowedDomains;
+                let sanitized = content;
+                // Neutralize @mentions to prevent unintended notifications
+                sanitized = neutralizeMentions(sanitized);
+                // Remove control characters (except newlines and tabs)
+                sanitized = sanitized.replace(/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/g, '');
+                // XML character escaping
+                sanitized = sanitized
+                  .replace(/&/g, '&amp;')   // Must be first to avoid double-escaping
+                  .replace(/</g, '&lt;')
+                  .replace(/>/g, '&gt;')
+                  .replace(/"/g, '&quot;')
+                  .replace(/'/g, '&apos;');
+                // URI filtering - replace non-https protocols with "(redacted)"
+                sanitized = sanitizeUrlProtocols(sanitized);
+                // Domain filtering for HTTPS URIs
+                sanitized = sanitizeUrlDomains(sanitized);
+                // Limit total length to prevent DoS (0.5MB max)
+                const maxLength = 524288;
+                if (sanitized.length > maxLength) {
+                  sanitized = sanitized.substring(0, maxLength) + '\n[Content truncated due to length]';
+                }
+                // Limit number of lines to prevent log flooding (65k max)
+                const lines = sanitized.split('\n');
+                const maxLines = 65000;
+                if (lines.length > maxLines) {
+                  sanitized = lines.slice(0, maxLines).join('\n') + '\n[Content truncated due to line count]';
+                }
+                // Remove ANSI escape sequences
+                sanitized = sanitized.replace(/\x1b\[[0-9;]*[mGKH]/g, '');
+                // Neutralize common bot trigger phrases
+                sanitized = neutralizeBotTriggers(sanitized);
+                // Trim excessive whitespace
+                return sanitized.trim();
+                /**
+                 * Remove unknown domains
+                 * @param {string} s - The string to process
+                 * @returns {string} The string with unknown domains redacted
+                 */
+                function sanitizeUrlDomains(s) {
+                  return s.replace(/\bhttps:\/\/([^\/\s\])}'"<>&\x00-\x1f]+)/gi, (match, domain) => {
+                    // Extract the hostname part (before first slash, colon, or other delimiter)
+                    const hostname = domain.split(/[\/:\?#]/)[0].toLowerCase();
+                    // Check if this domain or any parent domain is in the allowlist
+                    const isAllowed = allowedDomains.some(allowedDomain => {
+                      const normalizedAllowed = allowedDomain.toLowerCase();
+                      return hostname === normalizedAllowed || hostname.endsWith('.' + normalizedAllowed);
+                    });
+                    return isAllowed ? match : '(redacted)';
                   });
-                  return isAllowed ? match : '(redacted)';
-                });
-              }
-              /**
-               * Remove unknown protocols except https
-               * @param {string} s - The string to process
-               * @returns {string} The string with non-https protocols redacted
-               */
-              function sanitizeUrlProtocols(s) {
-                // Match both protocol:// and protocol: patterns
-                return s.replace(/\b(\w+):(?:\/\/)?[^\s\])}'"<>&\x00-\x1f]+/gi, (match, protocol) => {
-                  // Allow https (case insensitive), redact everything else
-                  return protocol.toLowerCase() === 'https' ? match : '(redacted)';
-                });
-              }
-              /**
-               * Neutralizes @mentions by wrapping them in backticks
-               * @param {string} s - The string to process
-               * @returns {string} The string with neutralized mentions
-               */
-              function neutralizeMentions(s) {
-                // Replace @name or @org/team outside code with `@name`
-                return s.replace(/(^|[^\w`])@([A-Za-z0-9](?:[A-Za-z0-9-]{0,37}[A-Za-z0-9])?(?:\/[A-Za-z0-9._-]+)?)/g,
-                  (_m, p1, p2) => `${p1}\`@${p2}\``);
+                }
+                /**
+                 * Remove unknown protocols except https
+                 * @param {string} s - The string to process
+                 * @returns {string} The string with non-https protocols redacted
+                 */
+                function sanitizeUrlProtocols(s) {
+                  // Match both protocol:// and protocol: patterns
+                  return s.replace(/\b(\w+):(?:\/\/)?[^\s\])}'"<>&\x00-\x1f]+/gi, (match, protocol) => {
+                    // Allow https (case insensitive), redact everything else
+                    return protocol.toLowerCase() === 'https' ? match : '(redacted)';
+                  });
+                }
+                /**
+                 * Neutralizes @mentions by wrapping them in backticks
+                 * @param {string} s - The string to process
+                 * @returns {string} The string with neutralized mentions
+                 */
+                function neutralizeMentions(s) {
+                  // Replace @name or @org/team outside code with `@name`
+                  return s.replace(/(^|[^\w`])@([A-Za-z0-9](?:[A-Za-z0-9-]{0,37}[A-Za-z0-9])?(?:\/[A-Za-z0-9._-]+)?)/g,
+                    (_m, p1, p2) => `${p1}\`@${p2}\``);
+                }
+                /**
+                 * Neutralizes bot trigger phrases by wrapping them in backticks
+                 * @param {string} s - The string to process
+                 * @returns {string} The string with neutralized bot triggers
+                 */
+                function neutralizeBotTriggers(s) {
+                  // Neutralize common bot trigger phrases like "fixes #123", "closes #asdfs", etc.
+                  return s.replace(/\b(fixes?|closes?|resolves?|fix|close|resolve)\s+#(\w+)/gi,
+                    (match, action, ref) => `\`${action} #${ref}\``);
+                }
               }
               /**
-               * Neutralizes bot trigger phrases by wrapping them in backticks
-               * @param {string} s - The string to process
-               * @returns {string} The string with neutralized bot triggers
+               * Gets the maximum allowed count for a given output type
+               * @param {string} itemType - The output item type
+               * @param {Object} config - The safe-outputs configuration
+               * @returns {number} The maximum allowed count
                */
-              function neutralizeBotTriggers(s) {
-                // Neutralize common bot trigger phrases like "fixes #123", "closes #asdfs", etc.
-                return s.replace(/\b(fixes?|closes?|resolves?|fix|close|resolve)\s+#(\w+)/gi,
-                  (match, action, ref) => `\`${action} #${ref}\``);
+              function getMaxAllowedForType(itemType, config) {
+                // Check if max is explicitly specified in config
+                if (config && config[itemType] && typeof config[itemType] === 'object' && config[itemType].max) {
+                  return config[itemType].max;
+                }
+                // Use default limits for plural-supported types
+                switch (itemType) {
+                  case 'create-issue':
+                    return 10; // Allow multiple issues
+                  case 'add-issue-comment':
+                    return 10; // Allow multiple comments
+                  case 'create-pull-request':
+                    return 1;  // Only one pull request allowed
+                  case 'add-issue-labels':
+                    return 1;  // Only one labels operation allowed
+                  default:
+                    return 1;  // Default to single item for unknown types
+                }
               }
-            }
-            async function main() {
-              const fs = require("fs");
               const outputFile = process.env.GITHUB_AW_SAFE_OUTPUTS;
               const safeOutputsConfig = process.env.GITHUB_AW_SAFE_OUTPUTS_CONFIG;
               if (!outputFile) {
@@ -522,10 +547,11 @@ jobs:
                     errors.push(`Line ${i + 1}: Unexpected output type '${itemType}'. Expected one of: ${Object.keys(expectedOutputTypes).join(', ')}`);
                     continue;
                   }
-                  // Check for duplicates based on type (since current format allows only one of each)
-                  const existingItem = parsedItems.find(existing => existing.type === itemType);
-                  if (existingItem) {
-                    errors.push(`Line ${i + 1}: Duplicate output type '${itemType}'. Only one item of each type is currently allowed.`);
+                  // Check for too many items of the same type
+                  const typeCount = parsedItems.filter(existing => existing.type === itemType).length;
+                  const maxAllowed = getMaxAllowedForType(itemType, expectedOutputTypes);
+                  if (typeCount >= maxAllowed) {
+                    errors.push(`Line ${i + 1}: Too many items of type '${itemType}'. Maximum allowed: ${maxAllowed}.`);
                     continue;
                   }
                   // Basic validation based on type
diff --git a/.github/workflows/test-codex-alias.lock.yml b/.github/workflows/test-codex-alias.lock.yml
index bbbcb75b92d..b9793b8b93b 100644
--- a/.github/workflows/test-codex-alias.lock.yml
+++ b/.github/workflows/test-codex-alias.lock.yml
@@ -666,113 +666,138 @@ jobs:
           GITHUB_AW_SAFE_OUTPUTS_CONFIG: "{\"add-issue-comment\":true}"
         with:
           script: |
-            /**
-             * Sanitizes content for safe output in GitHub Actions
-             * @param {string} content - The content to sanitize
-             * @returns {string} The sanitized content
-             */
-            function sanitizeContent(content) {
-              if (!content || typeof content !== 'string') {
-                return '';
-              }
-              // Read allowed domains from environment variable
-              const allowedDomainsEnv = process.env.GITHUB_AW_ALLOWED_DOMAINS;
-              const defaultAllowedDomains = [
-                'github.com',
-                'github.io',
-                'githubusercontent.com',
-                'githubassets.com',
-                'github.dev',
-                'codespaces.new'
-              ];
-              const allowedDomains = allowedDomainsEnv
-                ? allowedDomainsEnv.split(',').map(d => d.trim()).filter(d => d)
-                : defaultAllowedDomains;
-              let sanitized = content;
-              // Neutralize @mentions to prevent unintended notifications
-              sanitized = neutralizeMentions(sanitized);
-              // Remove control characters (except newlines and tabs)
-              sanitized = sanitized.replace(/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/g, '');
-              // XML character escaping
-              sanitized = sanitized
-                .replace(/&/g, '&amp;')   // Must be first to avoid double-escaping
-                .replace(/</g, '&lt;')
-                .replace(/>/g, '&gt;')
-                .replace(/"/g, '&quot;')
-                .replace(/'/g, '&apos;');
-              // URI filtering - replace non-https protocols with "(redacted)"
-              sanitized = sanitizeUrlProtocols(sanitized);
-              // Domain filtering for HTTPS URIs
-              sanitized = sanitizeUrlDomains(sanitized);
-              // Limit total length to prevent DoS (0.5MB max)
-              const maxLength = 524288;
-              if (sanitized.length > maxLength) {
-                sanitized = sanitized.substring(0, maxLength) + '\n[Content truncated due to length]';
-              }
-              // Limit number of lines to prevent log flooding (65k max)
-              const lines = sanitized.split('\n');
-              const maxLines = 65000;
-              if (lines.length > maxLines) {
-                sanitized = lines.slice(0, maxLines).join('\n') + '\n[Content truncated due to line count]';
-              }
-              // Remove ANSI escape sequences
-              sanitized = sanitized.replace(/\x1b\[[0-9;]*[mGKH]/g, '');
-              // Neutralize common bot trigger phrases
-              sanitized = neutralizeBotTriggers(sanitized);
-              // Trim excessive whitespace
-              return sanitized.trim();
+            async function main() {
+              const fs = require("fs");
               /**
-               * Remove unknown domains
-               * @param {string} s - The string to process
-               * @returns {string} The string with unknown domains redacted
+               * Sanitizes content for safe output in GitHub Actions
+               * @param {string} content - The content to sanitize
+               * @returns {string} The sanitized content
                */
-              function sanitizeUrlDomains(s) {
-                return s.replace(/\bhttps:\/\/([^\/\s\])}'"<>&\x00-\x1f]+)/gi, (match, domain) => {
-                  // Extract the hostname part (before first slash, colon, or other delimiter)
-                  const hostname = domain.split(/[\/:\?#]/)[0].toLowerCase();
-                  // Check if this domain or any parent domain is in the allowlist
-                  const isAllowed = allowedDomains.some(allowedDomain => {
-                    const normalizedAllowed = allowedDomain.toLowerCase();
-                    return hostname === normalizedAllowed || hostname.endsWith('.' + normalizedAllowed);
+              function sanitizeContent(content) {
+                if (!content || typeof content !== 'string') {
+                  return '';
+                }
+                // Read allowed domains from environment variable
+                const allowedDomainsEnv = process.env.GITHUB_AW_ALLOWED_DOMAINS;
+                const defaultAllowedDomains = [
+                  'github.com',
+                  'github.io',
+                  'githubusercontent.com',
+                  'githubassets.com',
+                  'github.dev',
+                  'codespaces.new'
+                ];
+                const allowedDomains = allowedDomainsEnv
+                  ? allowedDomainsEnv.split(',').map(d => d.trim()).filter(d => d)
+                  : defaultAllowedDomains;
+                let sanitized = content;
+                // Neutralize @mentions to prevent unintended notifications
+                sanitized = neutralizeMentions(sanitized);
+                // Remove control characters (except newlines and tabs)
+                sanitized = sanitized.replace(/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/g, '');
+                // XML character escaping
+                sanitized = sanitized
+                  .replace(/&/g, '&amp;')   // Must be first to avoid double-escaping
+                  .replace(/</g, '&lt;')
+                  .replace(/>/g, '&gt;')
+                  .replace(/"/g, '&quot;')
+                  .replace(/'/g, '&apos;');
+                // URI filtering - replace non-https protocols with "(redacted)"
+                sanitized = sanitizeUrlProtocols(sanitized);
+                // Domain filtering for HTTPS URIs
+                sanitized = sanitizeUrlDomains(sanitized);
+                // Limit total length to prevent DoS (0.5MB max)
+                const maxLength = 524288;
+                if (sanitized.length > maxLength) {
+                  sanitized = sanitized.substring(0, maxLength) + '\n[Content truncated due to length]';
+                }
+                // Limit number of lines to prevent log flooding (65k max)
+                const lines = sanitized.split('\n');
+                const maxLines = 65000;
+                if (lines.length > maxLines) {
+                  sanitized = lines.slice(0, maxLines).join('\n') + '\n[Content truncated due to line count]';
+                }
+                // Remove ANSI escape sequences
+                sanitized = sanitized.replace(/\x1b\[[0-9;]*[mGKH]/g, '');
+                // Neutralize common bot trigger phrases
+                sanitized = neutralizeBotTriggers(sanitized);
+                // Trim excessive whitespace
+                return sanitized.trim();
+                /**
+                 * Remove unknown domains
+                 * @param {string} s - The string to process
+                 * @returns {string} The string with unknown domains redacted
+                 */
+                function sanitizeUrlDomains(s) {
+                  return s.replace(/\bhttps:\/\/([^\/\s\])}'"<>&\x00-\x1f]+)/gi, (match, domain) => {
+                    // Extract the hostname part (before first slash, colon, or other delimiter)
+                    const hostname = domain.split(/[\/:\?#]/)[0].toLowerCase();
+                    // Check if this domain or any parent domain is in the allowlist
+                    const isAllowed = allowedDomains.some(allowedDomain => {
+                      const normalizedAllowed = allowedDomain.toLowerCase();
+                      return hostname === normalizedAllowed || hostname.endsWith('.' + normalizedAllowed);
+                    });
+                    return isAllowed ? match : '(redacted)';
                   });
-                  return isAllowed ? match : '(redacted)';
-                });
-              }
-              /**
-               * Remove unknown protocols except https
-               * @param {string} s - The string to process
-               * @returns {string} The string with non-https protocols redacted
-               */
-              function sanitizeUrlProtocols(s) {
-                // Match both protocol:// and protocol: patterns
-                return s.replace(/\b(\w+):(?:\/\/)?[^\s\])}'"<>&\x00-\x1f]+/gi, (match, protocol) => {
-                  // Allow https (case insensitive), redact everything else
-                  return protocol.toLowerCase() === 'https' ? match : '(redacted)';
-                });
-              }
-              /**
-               * Neutralizes @mentions by wrapping them in backticks
-               * @param {string} s - The string to process
-               * @returns {string} The string with neutralized mentions
-               */
-              function neutralizeMentions(s) {
-                // Replace @name or @org/team outside code with `@name`
-                return s.replace(/(^|[^\w`])@([A-Za-z0-9](?:[A-Za-z0-9-]{0,37}[A-Za-z0-9])?(?:\/[A-Za-z0-9._-]+)?)/g,
-                  (_m, p1, p2) => `${p1}\`@${p2}\``);
+                }
+                /**
+                 * Remove unknown protocols except https
+                 * @param {string} s - The string to process
+                 * @returns {string} The string with non-https protocols redacted
+                 */
+                function sanitizeUrlProtocols(s) {
+                  // Match both protocol:// and protocol: patterns
+                  return s.replace(/\b(\w+):(?:\/\/)?[^\s\])}'"<>&\x00-\x1f]+/gi, (match, protocol) => {
+                    // Allow https (case insensitive), redact everything else
+                    return protocol.toLowerCase() === 'https' ? match : '(redacted)';
+                  });
+                }
+                /**
+                 * Neutralizes @mentions by wrapping them in backticks
+                 * @param {string} s - The string to process
+                 * @returns {string} The string with neutralized mentions
+                 */
+                function neutralizeMentions(s) {
+                  // Replace @name or @org/team outside code with `@name`
+                  return s.replace(/(^|[^\w`])@([A-Za-z0-9](?:[A-Za-z0-9-]{0,37}[A-Za-z0-9])?(?:\/[A-Za-z0-9._-]+)?)/g,
+                    (_m, p1, p2) => `${p1}\`@${p2}\``);
+                }
+                /**
+                 * Neutralizes bot trigger phrases by wrapping them in backticks
+                 * @param {string} s - The string to process
+                 * @returns {string} The string with neutralized bot triggers
+                 */
+                function neutralizeBotTriggers(s) {
+                  // Neutralize common bot trigger phrases like "fixes #123", "closes #asdfs", etc.
+                  return s.replace(/\b(fixes?|closes?|resolves?|fix|close|resolve)\s+#(\w+)/gi,
+                    (match, action, ref) => `\`${action} #${ref}\``);
+                }
               }
               /**
-               * Neutralizes bot trigger phrases by wrapping them in backticks
-               * @param {string} s - The string to process
-               * @returns {string} The string with neutralized bot triggers
+               * Gets the maximum allowed count for a given output type
+               * @param {string} itemType - The output item type
+               * @param {Object} config - The safe-outputs configuration
+               * @returns {number} The maximum allowed count
                */
-              function neutralizeBotTriggers(s) {
-                // Neutralize common bot trigger phrases like "fixes #123", "closes #asdfs", etc.
-                return s.replace(/\b(fixes?|closes?|resolves?|fix|close|resolve)\s+#(\w+)/gi,
-                  (match, action, ref) => `\`${action} #${ref}\``);
+              function getMaxAllowedForType(itemType, config) {
+                // Check if max is explicitly specified in config
+                if (config && config[itemType] && typeof config[itemType] === 'object' && config[itemType].max) {
+                  return config[itemType].max;
+                }
+                // Use default limits for plural-supported types
+                switch (itemType) {
+                  case 'create-issue':
+                    return 10; // Allow multiple issues
+                  case 'add-issue-comment':
+                    return 10; // Allow multiple comments
+                  case 'create-pull-request':
+                    return 1;  // Only one pull request allowed
+                  case 'add-issue-labels':
+                    return 1;  // Only one labels operation allowed
+                  default:
+                    return 1;  // Default to single item for unknown types
+                }
               }
-            }
-            async function main() {
-              const fs = require("fs");
               const outputFile = process.env.GITHUB_AW_SAFE_OUTPUTS;
               const safeOutputsConfig = process.env.GITHUB_AW_SAFE_OUTPUTS_CONFIG;
               if (!outputFile) {
@@ -822,10 +847,11 @@ jobs:
                     errors.push(`Line ${i + 1}: Unexpected output type '${itemType}'. Expected one of: ${Object.keys(expectedOutputTypes).join(', ')}`);
                     continue;
                   }
-                  // Check for duplicates based on type (since current format allows only one of each)
-                  const existingItem = parsedItems.find(existing => existing.type === itemType);
-                  if (existingItem) {
-                    errors.push(`Line ${i + 1}: Duplicate output type '${itemType}'. Only one item of each type is currently allowed.`);
+                  // Check for too many items of the same type
+                  const typeCount = parsedItems.filter(existing => existing.type === itemType).length;
+                  const maxAllowed = getMaxAllowedForType(itemType, expectedOutputTypes);
+                  if (typeCount >= maxAllowed) {
+                    errors.push(`Line ${i + 1}: Too many items of type '${itemType}'. Maximum allowed: ${maxAllowed}.`);
                     continue;
                   }
                   // Basic validation based on type
@@ -1327,13 +1353,13 @@ jobs:
                 console.log('No valid items found in agent output');
                 return;
               }
-              // Find the add-issue-comment item
-              const commentItem = validatedOutput.items.find(/** @param {any} item */ item => item.type === 'add-issue-comment');
-              if (!commentItem) {
-                console.log('No add-issue-comment item found in agent output');
+              // Find all add-issue-comment items
+              const commentItems = validatedOutput.items.filter(/** @param {any} item */ item => item.type === 'add-issue-comment');
+              if (commentItems.length === 0) {
+                console.log('No add-issue-comment items found in agent output');
                 return;
               }
-              console.log('Found add-issue-comment item:', { bodyLength: commentItem.body.length });
+              console.log(`Found ${commentItems.length} add-issue-comment item(s)`);
               // Check if we're in an issue or pull request context
               const isIssueContext = context.eventName === 'issues' || context.eventName === 'issue_comment';
               const isPRContext = context.eventName === 'pull_request' || context.eventName === 'pull_request_review' || context.eventName === 'pull_request_review_comment';
@@ -1341,57 +1367,75 @@ jobs:
                 console.log('Not running in issue or pull request context, skipping comment creation');
                 return;
               }
-              // Determine the issue/PR number and comment endpoint
-              let issueNumber;
-              let commentEndpoint;
-              if (isIssueContext) {
-                if (context.payload.issue) {
-                  issueNumber = context.payload.issue.number;
-                  commentEndpoint = 'issues';
-                } else {
-                  console.log('Issue context detected but no issue found in payload');
-                  return;
+              const createdComments = [];
+              // Process each comment item
+              for (let i = 0; i < commentItems.length; i++) {
+                const commentItem = commentItems[i];
+                console.log(`Processing add-issue-comment item ${i + 1}/${commentItems.length}:`, { bodyLength: commentItem.body.length });
+                // Determine the issue/PR number and comment endpoint for this comment
+                let issueNumber;
+                let commentEndpoint;
+                if (isIssueContext) {
+                  if (context.payload.issue) {
+                    issueNumber = context.payload.issue.number;
+                    commentEndpoint = 'issues';
+                  } else {
+                    console.log('Issue context detected but no issue found in payload');
+                    continue;
+                  }
+                } else if (isPRContext) {
+                  if (context.payload.pull_request) {
+                    issueNumber = context.payload.pull_request.number;
+                    commentEndpoint = 'issues'; // PR comments use the issues API endpoint
+                  } else {
+                    console.log('Pull request context detected but no pull request found in payload');
+                    continue;
+                  }
                 }
-              } else if (isPRContext) {
-                if (context.payload.pull_request) {
-                  issueNumber = context.payload.pull_request.number;
-                  commentEndpoint = 'issues'; // PR comments use the issues API endpoint
-                } else {
-                  console.log('Pull request context detected but no pull request found in payload');
-                  return;
+                if (!issueNumber) {
+                  console.log('Could not determine issue or pull request number');
+                  continue;
+                }
+                // Extract body from the JSON item
+                let body = commentItem.body.trim();
+                // Add AI disclaimer with run id, run htmlurl
+                const runId = context.runId;
+                const runUrl = context.payload.repository 
+                  ? `${context.payload.repository.html_url}/actions/runs/${runId}`
+                  : `https://github.com/actions/runs/${runId}`;  
+                body += `\n\n> Generated by Agentic Workflow Run [${runId}](${runUrl})\n`;
+                console.log(`Creating comment on ${commentEndpoint} #${issueNumber}`);
+                console.log('Comment content length:', body.length);
+                try {
+                  // Create the comment using GitHub API
+                  const { data: comment } = await github.rest.issues.createComment({
+                    owner: context.repo.owner,
+                    repo: context.repo.repo,
+                    issue_number: issueNumber,
+                    body: body
+                  });
+                  console.log('Created comment #' + comment.id + ': ' + comment.html_url);
+                  createdComments.push(comment);
+                  // Set output for the last created comment (for backward compatibility)
+                  if (i === commentItems.length - 1) {
+                    core.setOutput('comment_id', comment.id);
+                    core.setOutput('comment_url', comment.html_url);
+                  }
+                } catch (error) {
+                  console.error(`✗ Failed to create comment:`, error instanceof Error ? error.message : String(error));
+                  throw error;
                 }
               }
-              if (!issueNumber) {
-                console.log('Could not determine issue or pull request number');
-                return;
+              // Write summary for all created comments
+              if (createdComments.length > 0) {
+                let summaryContent = '\n\n## GitHub Comments\n';
+                for (const comment of createdComments) {
+                  summaryContent += `- Comment #${comment.id}: [View Comment](${comment.html_url})\n`;
+                }
+                await core.summary.addRaw(summaryContent).write();
               }
-              // Extract body from the JSON item
-              let body = commentItem.body.trim();
-              // Add AI disclaimer with run id, run htmlurl
-              const runId = context.runId;
-              const runUrl = context.payload.repository 
-                ? `${context.payload.repository.html_url}/actions/runs/${runId}`
-                : `https://github.com/actions/runs/${runId}`;  
-              body += `\n\n> Generated by Agentic Workflow Run [${runId}](${runUrl})\n`;
-              console.log(`Creating comment on ${commentEndpoint} #${issueNumber}`);
-              console.log('Comment content length:', body.length);
-              // Create the comment using GitHub API
-              const { data: comment } = await github.rest.issues.createComment({
-                owner: context.repo.owner,
-                repo: context.repo.repo,
-                issue_number: issueNumber,
-                body: body
-              });
-              console.log('Created comment #' + comment.id + ': ' + comment.html_url);
-              // Set output for other jobs to use
-              core.setOutput('comment_id', comment.id);
-              core.setOutput('comment_url', comment.html_url);
-              // write comment id, url to the github_step_summary
-              await core.summary.addRaw(`
-            ## GitHub Comment
-            - Comment ID: ${comment.id}
-            - Comment URL: ${comment.html_url}
-            `).write();
+              console.log(`Successfully created ${createdComments.length} comment(s)`);
+              return createdComments;
             }
             await main();
 
diff --git a/.github/workflows/test-codex-create-issue.lock.yml b/.github/workflows/test-codex-create-issue.lock.yml
index 8f731cf8b02..21652888c48 100644
--- a/.github/workflows/test-codex-create-issue.lock.yml
+++ b/.github/workflows/test-codex-create-issue.lock.yml
@@ -195,113 +195,138 @@ jobs:
           GITHUB_AW_SAFE_OUTPUTS_CONFIG: "{\"create-issue\":true}"
         with:
           script: |
-            /**
-             * Sanitizes content for safe output in GitHub Actions
-             * @param {string} content - The content to sanitize
-             * @returns {string} The sanitized content
-             */
-            function sanitizeContent(content) {
-              if (!content || typeof content !== 'string') {
-                return '';
-              }
-              // Read allowed domains from environment variable
-              const allowedDomainsEnv = process.env.GITHUB_AW_ALLOWED_DOMAINS;
-              const defaultAllowedDomains = [
-                'github.com',
-                'github.io',
-                'githubusercontent.com',
-                'githubassets.com',
-                'github.dev',
-                'codespaces.new'
-              ];
-              const allowedDomains = allowedDomainsEnv
-                ? allowedDomainsEnv.split(',').map(d => d.trim()).filter(d => d)
-                : defaultAllowedDomains;
-              let sanitized = content;
-              // Neutralize @mentions to prevent unintended notifications
-              sanitized = neutralizeMentions(sanitized);
-              // Remove control characters (except newlines and tabs)
-              sanitized = sanitized.replace(/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/g, '');
-              // XML character escaping
-              sanitized = sanitized
-                .replace(/&/g, '&amp;')   // Must be first to avoid double-escaping
-                .replace(/</g, '&lt;')
-                .replace(/>/g, '&gt;')
-                .replace(/"/g, '&quot;')
-                .replace(/'/g, '&apos;');
-              // URI filtering - replace non-https protocols with "(redacted)"
-              sanitized = sanitizeUrlProtocols(sanitized);
-              // Domain filtering for HTTPS URIs
-              sanitized = sanitizeUrlDomains(sanitized);
-              // Limit total length to prevent DoS (0.5MB max)
-              const maxLength = 524288;
-              if (sanitized.length > maxLength) {
-                sanitized = sanitized.substring(0, maxLength) + '\n[Content truncated due to length]';
-              }
-              // Limit number of lines to prevent log flooding (65k max)
-              const lines = sanitized.split('\n');
-              const maxLines = 65000;
-              if (lines.length > maxLines) {
-                sanitized = lines.slice(0, maxLines).join('\n') + '\n[Content truncated due to line count]';
-              }
-              // Remove ANSI escape sequences
-              sanitized = sanitized.replace(/\x1b\[[0-9;]*[mGKH]/g, '');
-              // Neutralize common bot trigger phrases
-              sanitized = neutralizeBotTriggers(sanitized);
-              // Trim excessive whitespace
-              return sanitized.trim();
+            async function main() {
+              const fs = require("fs");
               /**
-               * Remove unknown domains
-               * @param {string} s - The string to process
-               * @returns {string} The string with unknown domains redacted
+               * Sanitizes content for safe output in GitHub Actions
+               * @param {string} content - The content to sanitize
+               * @returns {string} The sanitized content
                */
-              function sanitizeUrlDomains(s) {
-                return s.replace(/\bhttps:\/\/([^\/\s\])}'"<>&\x00-\x1f]+)/gi, (match, domain) => {
-                  // Extract the hostname part (before first slash, colon, or other delimiter)
-                  const hostname = domain.split(/[\/:\?#]/)[0].toLowerCase();
-                  // Check if this domain or any parent domain is in the allowlist
-                  const isAllowed = allowedDomains.some(allowedDomain => {
-                    const normalizedAllowed = allowedDomain.toLowerCase();
-                    return hostname === normalizedAllowed || hostname.endsWith('.' + normalizedAllowed);
+              function sanitizeContent(content) {
+                if (!content || typeof content !== 'string') {
+                  return '';
+                }
+                // Read allowed domains from environment variable
+                const allowedDomainsEnv = process.env.GITHUB_AW_ALLOWED_DOMAINS;
+                const defaultAllowedDomains = [
+                  'github.com',
+                  'github.io',
+                  'githubusercontent.com',
+                  'githubassets.com',
+                  'github.dev',
+                  'codespaces.new'
+                ];
+                const allowedDomains = allowedDomainsEnv
+                  ? allowedDomainsEnv.split(',').map(d => d.trim()).filter(d => d)
+                  : defaultAllowedDomains;
+                let sanitized = content;
+                // Neutralize @mentions to prevent unintended notifications
+                sanitized = neutralizeMentions(sanitized);
+                // Remove control characters (except newlines and tabs)
+                sanitized = sanitized.replace(/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/g, '');
+                // XML character escaping
+                sanitized = sanitized
+                  .replace(/&/g, '&amp;')   // Must be first to avoid double-escaping
+                  .replace(/</g, '&lt;')
+                  .replace(/>/g, '&gt;')
+                  .replace(/"/g, '&quot;')
+                  .replace(/'/g, '&apos;');
+                // URI filtering - replace non-https protocols with "(redacted)"
+                sanitized = sanitizeUrlProtocols(sanitized);
+                // Domain filtering for HTTPS URIs
+                sanitized = sanitizeUrlDomains(sanitized);
+                // Limit total length to prevent DoS (0.5MB max)
+                const maxLength = 524288;
+                if (sanitized.length > maxLength) {
+                  sanitized = sanitized.substring(0, maxLength) + '\n[Content truncated due to length]';
+                }
+                // Limit number of lines to prevent log flooding (65k max)
+                const lines = sanitized.split('\n');
+                const maxLines = 65000;
+                if (lines.length > maxLines) {
+                  sanitized = lines.slice(0, maxLines).join('\n') + '\n[Content truncated due to line count]';
+                }
+                // Remove ANSI escape sequences
+                sanitized = sanitized.replace(/\x1b\[[0-9;]*[mGKH]/g, '');
+                // Neutralize common bot trigger phrases
+                sanitized = neutralizeBotTriggers(sanitized);
+                // Trim excessive whitespace
+                return sanitized.trim();
+                /**
+                 * Remove unknown domains
+                 * @param {string} s - The string to process
+                 * @returns {string} The string with unknown domains redacted
+                 */
+                function sanitizeUrlDomains(s) {
+                  return s.replace(/\bhttps:\/\/([^\/\s\])}'"<>&\x00-\x1f]+)/gi, (match, domain) => {
+                    // Extract the hostname part (before first slash, colon, or other delimiter)
+                    const hostname = domain.split(/[\/:\?#]/)[0].toLowerCase();
+                    // Check if this domain or any parent domain is in the allowlist
+                    const isAllowed = allowedDomains.some(allowedDomain => {
+                      const normalizedAllowed = allowedDomain.toLowerCase();
+                      return hostname === normalizedAllowed || hostname.endsWith('.' + normalizedAllowed);
+                    });
+                    return isAllowed ? match : '(redacted)';
                   });
-                  return isAllowed ? match : '(redacted)';
-                });
-              }
-              /**
-               * Remove unknown protocols except https
-               * @param {string} s - The string to process
-               * @returns {string} The string with non-https protocols redacted
-               */
-              function sanitizeUrlProtocols(s) {
-                // Match both protocol:// and protocol: patterns
-                return s.replace(/\b(\w+):(?:\/\/)?[^\s\])}'"<>&\x00-\x1f]+/gi, (match, protocol) => {
-                  // Allow https (case insensitive), redact everything else
-                  return protocol.toLowerCase() === 'https' ? match : '(redacted)';
-                });
-              }
-              /**
-               * Neutralizes @mentions by wrapping them in backticks
-               * @param {string} s - The string to process
-               * @returns {string} The string with neutralized mentions
-               */
-              function neutralizeMentions(s) {
-                // Replace @name or @org/team outside code with `@name`
-                return s.replace(/(^|[^\w`])@([A-Za-z0-9](?:[A-Za-z0-9-]{0,37}[A-Za-z0-9])?(?:\/[A-Za-z0-9._-]+)?)/g,
-                  (_m, p1, p2) => `${p1}\`@${p2}\``);
+                }
+                /**
+                 * Remove unknown protocols except https
+                 * @param {string} s - The string to process
+                 * @returns {string} The string with non-https protocols redacted
+                 */
+                function sanitizeUrlProtocols(s) {
+                  // Match both protocol:// and protocol: patterns
+                  return s.replace(/\b(\w+):(?:\/\/)?[^\s\])}'"<>&\x00-\x1f]+/gi, (match, protocol) => {
+                    // Allow https (case insensitive), redact everything else
+                    return protocol.toLowerCase() === 'https' ? match : '(redacted)';
+                  });
+                }
+                /**
+                 * Neutralizes @mentions by wrapping them in backticks
+                 * @param {string} s - The string to process
+                 * @returns {string} The string with neutralized mentions
+                 */
+                function neutralizeMentions(s) {
+                  // Replace @name or @org/team outside code with `@name`
+                  return s.replace(/(^|[^\w`])@([A-Za-z0-9](?:[A-Za-z0-9-]{0,37}[A-Za-z0-9])?(?:\/[A-Za-z0-9._-]+)?)/g,
+                    (_m, p1, p2) => `${p1}\`@${p2}\``);
+                }
+                /**
+                 * Neutralizes bot trigger phrases by wrapping them in backticks
+                 * @param {string} s - The string to process
+                 * @returns {string} The string with neutralized bot triggers
+                 */
+                function neutralizeBotTriggers(s) {
+                  // Neutralize common bot trigger phrases like "fixes #123", "closes #asdfs", etc.
+                  return s.replace(/\b(fixes?|closes?|resolves?|fix|close|resolve)\s+#(\w+)/gi,
+                    (match, action, ref) => `\`${action} #${ref}\``);
+                }
               }
               /**
-               * Neutralizes bot trigger phrases by wrapping them in backticks
-               * @param {string} s - The string to process
-               * @returns {string} The string with neutralized bot triggers
+               * Gets the maximum allowed count for a given output type
+               * @param {string} itemType - The output item type
+               * @param {Object} config - The safe-outputs configuration
+               * @returns {number} The maximum allowed count
                */
-              function neutralizeBotTriggers(s) {
-                // Neutralize common bot trigger phrases like "fixes #123", "closes #asdfs", etc.
-                return s.replace(/\b(fixes?|closes?|resolves?|fix|close|resolve)\s+#(\w+)/gi,
-                  (match, action, ref) => `\`${action} #${ref}\``);
+              function getMaxAllowedForType(itemType, config) {
+                // Check if max is explicitly specified in config
+                if (config && config[itemType] && typeof config[itemType] === 'object' && config[itemType].max) {
+                  return config[itemType].max;
+                }
+                // Use default limits for plural-supported types
+                switch (itemType) {
+                  case 'create-issue':
+                    return 10; // Allow multiple issues
+                  case 'add-issue-comment':
+                    return 10; // Allow multiple comments
+                  case 'create-pull-request':
+                    return 1;  // Only one pull request allowed
+                  case 'add-issue-labels':
+                    return 1;  // Only one labels operation allowed
+                  default:
+                    return 1;  // Default to single item for unknown types
+                }
               }
-            }
-            async function main() {
-              const fs = require("fs");
               const outputFile = process.env.GITHUB_AW_SAFE_OUTPUTS;
               const safeOutputsConfig = process.env.GITHUB_AW_SAFE_OUTPUTS_CONFIG;
               if (!outputFile) {
@@ -351,10 +376,11 @@ jobs:
                     errors.push(`Line ${i + 1}: Unexpected output type '${itemType}'. Expected one of: ${Object.keys(expectedOutputTypes).join(', ')}`);
                     continue;
                   }
-                  // Check for duplicates based on type (since current format allows only one of each)
-                  const existingItem = parsedItems.find(existing => existing.type === itemType);
-                  if (existingItem) {
-                    errors.push(`Line ${i + 1}: Duplicate output type '${itemType}'. Only one item of each type is currently allowed.`);
+                  // Check for too many items of the same type
+                  const typeCount = parsedItems.filter(existing => existing.type === itemType).length;
+                  const maxAllowed = getMaxAllowedForType(itemType, expectedOutputTypes);
+                  if (typeCount >= maxAllowed) {
+                    errors.push(`Line ${i + 1}: Too many items of type '${itemType}'. Maximum allowed: ${maxAllowed}.`);
                     continue;
                   }
                   // Basic validation based on type
@@ -801,83 +827,101 @@ jobs:
                 console.log('No valid items found in agent output');
                 return;
               }
-              // Find the create-issue item
-              const createIssueItem = validatedOutput.items.find(/** @param {any} item */ item => item.type === 'create-issue');
-              if (!createIssueItem) {
-                console.log('No create-issue item found in agent output');
+              // Find all create-issue items
+              const createIssueItems = validatedOutput.items.filter(/** @param {any} item */ item => item.type === 'create-issue');
+              if (createIssueItems.length === 0) {
+                console.log('No create-issue items found in agent output');
                 return;
               }
-              console.log('Found create-issue item:', { title: createIssueItem.title, bodyLength: createIssueItem.body.length });
+              console.log(`Found ${createIssueItems.length} create-issue item(s)`);
               // Check if we're in an issue context (triggered by an issue event)
               const parentIssueNumber = context.payload?.issue?.number;
-              // Parse labels from environment variable (comma-separated string) or from the JSON item
+              // Parse labels from environment variable (comma-separated string)
               const labelsEnv = process.env.GITHUB_AW_ISSUE_LABELS;
-              let labels = labelsEnv ? labelsEnv.split(',').map(/** @param {string} label */ label => label.trim()).filter(/** @param {string} label */ label => label) : [];
-              // If the item has labels, use those instead (or merge them)
-              if (createIssueItem.labels && Array.isArray(createIssueItem.labels)) {
-                labels = [...labels, ...createIssueItem.labels].filter(Boolean);
-              }
-              // Extract title and body from the JSON item
-              let title = createIssueItem.title ? createIssueItem.title.trim() : '';
-              let bodyLines = createIssueItem.body.split('\n');
-              // If no title was found, use the body content as title (or a default)
-              if (!title) {
-                title = createIssueItem.body || 'Agent Output';
-              }
-              // Apply title prefix if provided via environment variable
-              const titlePrefix = process.env.GITHUB_AW_ISSUE_TITLE_PREFIX;
-              if (titlePrefix && !title.startsWith(titlePrefix)) {
-                title = titlePrefix + title;
-              }
-              if (parentIssueNumber) {
-                console.log('Detected issue context, parent issue #' + parentIssueNumber);
-                // Add reference to parent issue in the child issue body
-                bodyLines.push(`Related to #${parentIssueNumber}`);
-              }
-              // Add AI disclaimer with run id, run htmlurl
-              // Add AI disclaimer with workflow run information
-              const runId = context.runId;
-              const runUrl = context.payload.repository 
-                ? `${context.payload.repository.html_url}/actions/runs/${runId}`
-                : `https://github.com/actions/runs/${runId}`;  
-              bodyLines.push(``, ``, `> Generated by Agentic Workflow Run [${runId}](${runUrl})`, '');
-              // Prepare the body content
-              const body = bodyLines.join('\n').trim();
-              console.log('Creating issue with title:', title);
-              console.log('Labels:', labels);
-              console.log('Body length:', body.length);
-              // Create the issue using GitHub API
-              const { data: issue } = await github.rest.issues.create({
-                owner: context.repo.owner,
-                repo: context.repo.repo,
-                title: title,
-                body: body,
-                labels: labels
-              });
-              console.log('Created issue #' + issue.number + ': ' + issue.html_url);
-              // If we have a parent issue, add a comment to it referencing the new child issue
-              if (parentIssueNumber) {
+              let envLabels = labelsEnv ? labelsEnv.split(',').map(/** @param {string} label */ label => label.trim()).filter(/** @param {string} label */ label => label) : [];
+              const createdIssues = [];
+              // Process each create-issue item
+              for (let i = 0; i < createIssueItems.length; i++) {
+                const createIssueItem = createIssueItems[i];
+                console.log(`Processing create-issue item ${i + 1}/${createIssueItems.length}:`, { title: createIssueItem.title, bodyLength: createIssueItem.body.length });
+                // Merge environment labels with item-specific labels
+                let labels = [...envLabels];
+                if (createIssueItem.labels && Array.isArray(createIssueItem.labels)) {
+                  labels = [...labels, ...createIssueItem.labels].filter(Boolean);
+                }
+                // Extract title and body from the JSON item
+                let title = createIssueItem.title ? createIssueItem.title.trim() : '';
+                let bodyLines = createIssueItem.body.split('\n');
+                // If no title was found, use the body content as title (or a default)
+                if (!title) {
+                  title = createIssueItem.body || 'Agent Output';
+                }
+                // Apply title prefix if provided via environment variable
+                const titlePrefix = process.env.GITHUB_AW_ISSUE_TITLE_PREFIX;
+                if (titlePrefix && !title.startsWith(titlePrefix)) {
+                  title = titlePrefix + title;
+                }
+                if (parentIssueNumber) {
+                  console.log('Detected issue context, parent issue #' + parentIssueNumber);
+                  // Add reference to parent issue in the child issue body
+                  bodyLines.push(`Related to #${parentIssueNumber}`);
+                }
+                // Add AI disclaimer with run id, run htmlurl
+                // Add AI disclaimer with workflow run information
+                const runId = context.runId;
+                const runUrl = context.payload.repository 
+                  ? `${context.payload.repository.html_url}/actions/runs/${runId}`
+                  : `https://github.com/actions/runs/${runId}`;  
+                bodyLines.push(``, ``, `> Generated by Agentic Workflow Run [${runId}](${runUrl})`, '');
+                // Prepare the body content
+                const body = bodyLines.join('\n').trim();
+                console.log('Creating issue with title:', title);
+                console.log('Labels:', labels);
+                console.log('Body length:', body.length);
                 try {
-                  await github.rest.issues.createComment({
+                  // Create the issue using GitHub API
+                  const { data: issue } = await github.rest.issues.create({
                     owner: context.repo.owner,
                     repo: context.repo.repo,
-                    issue_number: parentIssueNumber,
-                    body: `Created related issue: #${issue.number}`
+                    title: title,
+                    body: body,
+                    labels: labels
                   });
-                  console.log('Added comment to parent issue #' + parentIssueNumber);
+                  console.log('Created issue #' + issue.number + ': ' + issue.html_url);
+                  createdIssues.push(issue);
+                  // If we have a parent issue, add a comment to it referencing the new child issue
+                  if (parentIssueNumber) {
+                    try {
+                      await github.rest.issues.createComment({
+                        owner: context.repo.owner,
+                        repo: context.repo.repo,
+                        issue_number: parentIssueNumber,
+                        body: `Created related issue: #${issue.number}`
+                      });
+                      console.log('Added comment to parent issue #' + parentIssueNumber);
+                    } catch (error) {
+                      console.log('Warning: Could not add comment to parent issue:', error instanceof Error ? error.message : String(error));
+                    }
+                  }
+                  // Set output for the last created issue (for backward compatibility)
+                  if (i === createIssueItems.length - 1) {
+                    core.setOutput('issue_number', issue.number);
+                    core.setOutput('issue_url', issue.html_url);
+                  }
                 } catch (error) {
-                  console.log('Warning: Could not add comment to parent issue:', error instanceof Error ? error.message : String(error));
+                  console.error(`✗ Failed to create issue "${title}":`, error instanceof Error ? error.message : String(error));
+                  throw error;
+                }
+              }
+              // Write summary for all created issues
+              if (createdIssues.length > 0) {
+                let summaryContent = '\n\n## GitHub Issues\n';
+                for (const issue of createdIssues) {
+                  summaryContent += `- Issue #${issue.number}: [${issue.title}](${issue.html_url})\n`;
                 }
+                await core.summary.addRaw(summaryContent).write();
               }
-              // Set output for other jobs to use
-              core.setOutput('issue_number', issue.number);
-              core.setOutput('issue_url', issue.html_url);
-              // write issue to summary
-              await core.summary.addRaw(`
-            ## GitHub Issue
-            - Issue ID: ${issue.number}
-            - Issue URL: ${issue.html_url}
-            `).write();
+              console.log(`Successfully created ${createdIssues.length} issue(s)`);
             }
             await main();
 
diff --git a/.github/workflows/test-codex-create-pull-request.lock.yml b/.github/workflows/test-codex-create-pull-request.lock.yml
index c48088188f4..70bea818799 100644
--- a/.github/workflows/test-codex-create-pull-request.lock.yml
+++ b/.github/workflows/test-codex-create-pull-request.lock.yml
@@ -204,113 +204,138 @@ jobs:
           GITHUB_AW_SAFE_OUTPUTS_CONFIG: "{\"create-pull-request\":true}"
         with:
           script: |
-            /**
-             * Sanitizes content for safe output in GitHub Actions
-             * @param {string} content - The content to sanitize
-             * @returns {string} The sanitized content
-             */
-            function sanitizeContent(content) {
-              if (!content || typeof content !== 'string') {
-                return '';
-              }
-              // Read allowed domains from environment variable
-              const allowedDomainsEnv = process.env.GITHUB_AW_ALLOWED_DOMAINS;
-              const defaultAllowedDomains = [
-                'github.com',
-                'github.io',
-                'githubusercontent.com',
-                'githubassets.com',
-                'github.dev',
-                'codespaces.new'
-              ];
-              const allowedDomains = allowedDomainsEnv
-                ? allowedDomainsEnv.split(',').map(d => d.trim()).filter(d => d)
-                : defaultAllowedDomains;
-              let sanitized = content;
-              // Neutralize @mentions to prevent unintended notifications
-              sanitized = neutralizeMentions(sanitized);
-              // Remove control characters (except newlines and tabs)
-              sanitized = sanitized.replace(/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/g, '');
-              // XML character escaping
-              sanitized = sanitized
-                .replace(/&/g, '&amp;')   // Must be first to avoid double-escaping
-                .replace(/</g, '&lt;')
-                .replace(/>/g, '&gt;')
-                .replace(/"/g, '&quot;')
-                .replace(/'/g, '&apos;');
-              // URI filtering - replace non-https protocols with "(redacted)"
-              sanitized = sanitizeUrlProtocols(sanitized);
-              // Domain filtering for HTTPS URIs
-              sanitized = sanitizeUrlDomains(sanitized);
-              // Limit total length to prevent DoS (0.5MB max)
-              const maxLength = 524288;
-              if (sanitized.length > maxLength) {
-                sanitized = sanitized.substring(0, maxLength) + '\n[Content truncated due to length]';
-              }
-              // Limit number of lines to prevent log flooding (65k max)
-              const lines = sanitized.split('\n');
-              const maxLines = 65000;
-              if (lines.length > maxLines) {
-                sanitized = lines.slice(0, maxLines).join('\n') + '\n[Content truncated due to line count]';
-              }
-              // Remove ANSI escape sequences
-              sanitized = sanitized.replace(/\x1b\[[0-9;]*[mGKH]/g, '');
-              // Neutralize common bot trigger phrases
-              sanitized = neutralizeBotTriggers(sanitized);
-              // Trim excessive whitespace
-              return sanitized.trim();
+            async function main() {
+              const fs = require("fs");
               /**
-               * Remove unknown domains
-               * @param {string} s - The string to process
-               * @returns {string} The string with unknown domains redacted
+               * Sanitizes content for safe output in GitHub Actions
+               * @param {string} content - The content to sanitize
+               * @returns {string} The sanitized content
                */
-              function sanitizeUrlDomains(s) {
-                return s.replace(/\bhttps:\/\/([^\/\s\])}'"<>&\x00-\x1f]+)/gi, (match, domain) => {
-                  // Extract the hostname part (before first slash, colon, or other delimiter)
-                  const hostname = domain.split(/[\/:\?#]/)[0].toLowerCase();
-                  // Check if this domain or any parent domain is in the allowlist
-                  const isAllowed = allowedDomains.some(allowedDomain => {
-                    const normalizedAllowed = allowedDomain.toLowerCase();
-                    return hostname === normalizedAllowed || hostname.endsWith('.' + normalizedAllowed);
+              function sanitizeContent(content) {
+                if (!content || typeof content !== 'string') {
+                  return '';
+                }
+                // Read allowed domains from environment variable
+                const allowedDomainsEnv = process.env.GITHUB_AW_ALLOWED_DOMAINS;
+                const defaultAllowedDomains = [
+                  'github.com',
+                  'github.io',
+                  'githubusercontent.com',
+                  'githubassets.com',
+                  'github.dev',
+                  'codespaces.new'
+                ];
+                const allowedDomains = allowedDomainsEnv
+                  ? allowedDomainsEnv.split(',').map(d => d.trim()).filter(d => d)
+                  : defaultAllowedDomains;
+                let sanitized = content;
+                // Neutralize @mentions to prevent unintended notifications
+                sanitized = neutralizeMentions(sanitized);
+                // Remove control characters (except newlines and tabs)
+                sanitized = sanitized.replace(/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/g, '');
+                // XML character escaping
+                sanitized = sanitized
+                  .replace(/&/g, '&amp;')   // Must be first to avoid double-escaping
+                  .replace(/</g, '&lt;')
+                  .replace(/>/g, '&gt;')
+                  .replace(/"/g, '&quot;')
+                  .replace(/'/g, '&apos;');
+                // URI filtering - replace non-https protocols with "(redacted)"
+                sanitized = sanitizeUrlProtocols(sanitized);
+                // Domain filtering for HTTPS URIs
+                sanitized = sanitizeUrlDomains(sanitized);
+                // Limit total length to prevent DoS (0.5MB max)
+                const maxLength = 524288;
+                if (sanitized.length > maxLength) {
+                  sanitized = sanitized.substring(0, maxLength) + '\n[Content truncated due to length]';
+                }
+                // Limit number of lines to prevent log flooding (65k max)
+                const lines = sanitized.split('\n');
+                const maxLines = 65000;
+                if (lines.length > maxLines) {
+                  sanitized = lines.slice(0, maxLines).join('\n') + '\n[Content truncated due to line count]';
+                }
+                // Remove ANSI escape sequences
+                sanitized = sanitized.replace(/\x1b\[[0-9;]*[mGKH]/g, '');
+                // Neutralize common bot trigger phrases
+                sanitized = neutralizeBotTriggers(sanitized);
+                // Trim excessive whitespace
+                return sanitized.trim();
+                /**
+                 * Remove unknown domains
+                 * @param {string} s - The string to process
+                 * @returns {string} The string with unknown domains redacted
+                 */
+                function sanitizeUrlDomains(s) {
+                  return s.replace(/\bhttps:\/\/([^\/\s\])}'"<>&\x00-\x1f]+)/gi, (match, domain) => {
+                    // Extract the hostname part (before first slash, colon, or other delimiter)
+                    const hostname = domain.split(/[\/:\?#]/)[0].toLowerCase();
+                    // Check if this domain or any parent domain is in the allowlist
+                    const isAllowed = allowedDomains.some(allowedDomain => {
+                      const normalizedAllowed = allowedDomain.toLowerCase();
+                      return hostname === normalizedAllowed || hostname.endsWith('.' + normalizedAllowed);
+                    });
+                    return isAllowed ? match : '(redacted)';
                   });
-                  return isAllowed ? match : '(redacted)';
-                });
-              }
-              /**
-               * Remove unknown protocols except https
-               * @param {string} s - The string to process
-               * @returns {string} The string with non-https protocols redacted
-               */
-              function sanitizeUrlProtocols(s) {
-                // Match both protocol:// and protocol: patterns
-                return s.replace(/\b(\w+):(?:\/\/)?[^\s\])}'"<>&\x00-\x1f]+/gi, (match, protocol) => {
-                  // Allow https (case insensitive), redact everything else
-                  return protocol.toLowerCase() === 'https' ? match : '(redacted)';
-                });
-              }
-              /**
-               * Neutralizes @mentions by wrapping them in backticks
-               * @param {string} s - The string to process
-               * @returns {string} The string with neutralized mentions
-               */
-              function neutralizeMentions(s) {
-                // Replace @name or @org/team outside code with `@name`
-                return s.replace(/(^|[^\w`])@([A-Za-z0-9](?:[A-Za-z0-9-]{0,37}[A-Za-z0-9])?(?:\/[A-Za-z0-9._-]+)?)/g,
-                  (_m, p1, p2) => `${p1}\`@${p2}\``);
+                }
+                /**
+                 * Remove unknown protocols except https
+                 * @param {string} s - The string to process
+                 * @returns {string} The string with non-https protocols redacted
+                 */
+                function sanitizeUrlProtocols(s) {
+                  // Match both protocol:// and protocol: patterns
+                  return s.replace(/\b(\w+):(?:\/\/)?[^\s\])}'"<>&\x00-\x1f]+/gi, (match, protocol) => {
+                    // Allow https (case insensitive), redact everything else
+                    return protocol.toLowerCase() === 'https' ? match : '(redacted)';
+                  });
+                }
+                /**
+                 * Neutralizes @mentions by wrapping them in backticks
+                 * @param {string} s - The string to process
+                 * @returns {string} The string with neutralized mentions
+                 */
+                function neutralizeMentions(s) {
+                  // Replace @name or @org/team outside code with `@name`
+                  return s.replace(/(^|[^\w`])@([A-Za-z0-9](?:[A-Za-z0-9-]{0,37}[A-Za-z0-9])?(?:\/[A-Za-z0-9._-]+)?)/g,
+                    (_m, p1, p2) => `${p1}\`@${p2}\``);
+                }
+                /**
+                 * Neutralizes bot trigger phrases by wrapping them in backticks
+                 * @param {string} s - The string to process
+                 * @returns {string} The string with neutralized bot triggers
+                 */
+                function neutralizeBotTriggers(s) {
+                  // Neutralize common bot trigger phrases like "fixes #123", "closes #asdfs", etc.
+                  return s.replace(/\b(fixes?|closes?|resolves?|fix|close|resolve)\s+#(\w+)/gi,
+                    (match, action, ref) => `\`${action} #${ref}\``);
+                }
               }
               /**
-               * Neutralizes bot trigger phrases by wrapping them in backticks
-               * @param {string} s - The string to process
-               * @returns {string} The string with neutralized bot triggers
+               * Gets the maximum allowed count for a given output type
+               * @param {string} itemType - The output item type
+               * @param {Object} config - The safe-outputs configuration
+               * @returns {number} The maximum allowed count
                */
-              function neutralizeBotTriggers(s) {
-                // Neutralize common bot trigger phrases like "fixes #123", "closes #asdfs", etc.
-                return s.replace(/\b(fixes?|closes?|resolves?|fix|close|resolve)\s+#(\w+)/gi,
-                  (match, action, ref) => `\`${action} #${ref}\``);
+              function getMaxAllowedForType(itemType, config) {
+                // Check if max is explicitly specified in config
+                if (config && config[itemType] && typeof config[itemType] === 'object' && config[itemType].max) {
+                  return config[itemType].max;
+                }
+                // Use default limits for plural-supported types
+                switch (itemType) {
+                  case 'create-issue':
+                    return 10; // Allow multiple issues
+                  case 'add-issue-comment':
+                    return 10; // Allow multiple comments
+                  case 'create-pull-request':
+                    return 1;  // Only one pull request allowed
+                  case 'add-issue-labels':
+                    return 1;  // Only one labels operation allowed
+                  default:
+                    return 1;  // Default to single item for unknown types
+                }
               }
-            }
-            async function main() {
-              const fs = require("fs");
               const outputFile = process.env.GITHUB_AW_SAFE_OUTPUTS;
               const safeOutputsConfig = process.env.GITHUB_AW_SAFE_OUTPUTS_CONFIG;
               if (!outputFile) {
@@ -360,10 +385,11 @@ jobs:
                     errors.push(`Line ${i + 1}: Unexpected output type '${itemType}'. Expected one of: ${Object.keys(expectedOutputTypes).join(', ')}`);
                     continue;
                   }
-                  // Check for duplicates based on type (since current format allows only one of each)
-                  const existingItem = parsedItems.find(existing => existing.type === itemType);
-                  if (existingItem) {
-                    errors.push(`Line ${i + 1}: Duplicate output type '${itemType}'. Only one item of each type is currently allowed.`);
+                  // Check for too many items of the same type
+                  const typeCount = parsedItems.filter(existing => existing.type === itemType).length;
+                  const maxAllowed = getMaxAllowedForType(itemType, expectedOutputTypes);
+                  if (typeCount >= maxAllowed) {
+                    errors.push(`Line ${i + 1}: Too many items of type '${itemType}'. Maximum allowed: ${maxAllowed}.`);
                     continue;
                   }
                   // Basic validation based on type
diff --git a/.github/workflows/test-codex-mcp.lock.yml b/.github/workflows/test-codex-mcp.lock.yml
index b6ea3a408f1..68eaa1ce117 100644
--- a/.github/workflows/test-codex-mcp.lock.yml
+++ b/.github/workflows/test-codex-mcp.lock.yml
@@ -385,113 +385,138 @@ jobs:
           GITHUB_AW_SAFE_OUTPUTS_CONFIG: "{\"create-issue\":true}"
         with:
           script: |
-            /**
-             * Sanitizes content for safe output in GitHub Actions
-             * @param {string} content - The content to sanitize
-             * @returns {string} The sanitized content
-             */
-            function sanitizeContent(content) {
-              if (!content || typeof content !== 'string') {
-                return '';
-              }
-              // Read allowed domains from environment variable
-              const allowedDomainsEnv = process.env.GITHUB_AW_ALLOWED_DOMAINS;
-              const defaultAllowedDomains = [
-                'github.com',
-                'github.io',
-                'githubusercontent.com',
-                'githubassets.com',
-                'github.dev',
-                'codespaces.new'
-              ];
-              const allowedDomains = allowedDomainsEnv
-                ? allowedDomainsEnv.split(',').map(d => d.trim()).filter(d => d)
-                : defaultAllowedDomains;
-              let sanitized = content;
-              // Neutralize @mentions to prevent unintended notifications
-              sanitized = neutralizeMentions(sanitized);
-              // Remove control characters (except newlines and tabs)
-              sanitized = sanitized.replace(/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/g, '');
-              // XML character escaping
-              sanitized = sanitized
-                .replace(/&/g, '&amp;')   // Must be first to avoid double-escaping
-                .replace(/</g, '&lt;')
-                .replace(/>/g, '&gt;')
-                .replace(/"/g, '&quot;')
-                .replace(/'/g, '&apos;');
-              // URI filtering - replace non-https protocols with "(redacted)"
-              sanitized = sanitizeUrlProtocols(sanitized);
-              // Domain filtering for HTTPS URIs
-              sanitized = sanitizeUrlDomains(sanitized);
-              // Limit total length to prevent DoS (0.5MB max)
-              const maxLength = 524288;
-              if (sanitized.length > maxLength) {
-                sanitized = sanitized.substring(0, maxLength) + '\n[Content truncated due to length]';
-              }
-              // Limit number of lines to prevent log flooding (65k max)
-              const lines = sanitized.split('\n');
-              const maxLines = 65000;
-              if (lines.length > maxLines) {
-                sanitized = lines.slice(0, maxLines).join('\n') + '\n[Content truncated due to line count]';
-              }
-              // Remove ANSI escape sequences
-              sanitized = sanitized.replace(/\x1b\[[0-9;]*[mGKH]/g, '');
-              // Neutralize common bot trigger phrases
-              sanitized = neutralizeBotTriggers(sanitized);
-              // Trim excessive whitespace
-              return sanitized.trim();
+            async function main() {
+              const fs = require("fs");
               /**
-               * Remove unknown domains
-               * @param {string} s - The string to process
-               * @returns {string} The string with unknown domains redacted
+               * Sanitizes content for safe output in GitHub Actions
+               * @param {string} content - The content to sanitize
+               * @returns {string} The sanitized content
                */
-              function sanitizeUrlDomains(s) {
-                return s.replace(/\bhttps:\/\/([^\/\s\])}'"<>&\x00-\x1f]+)/gi, (match, domain) => {
-                  // Extract the hostname part (before first slash, colon, or other delimiter)
-                  const hostname = domain.split(/[\/:\?#]/)[0].toLowerCase();
-                  // Check if this domain or any parent domain is in the allowlist
-                  const isAllowed = allowedDomains.some(allowedDomain => {
-                    const normalizedAllowed = allowedDomain.toLowerCase();
-                    return hostname === normalizedAllowed || hostname.endsWith('.' + normalizedAllowed);
+              function sanitizeContent(content) {
+                if (!content || typeof content !== 'string') {
+                  return '';
+                }
+                // Read allowed domains from environment variable
+                const allowedDomainsEnv = process.env.GITHUB_AW_ALLOWED_DOMAINS;
+                const defaultAllowedDomains = [
+                  'github.com',
+                  'github.io',
+                  'githubusercontent.com',
+                  'githubassets.com',
+                  'github.dev',
+                  'codespaces.new'
+                ];
+                const allowedDomains = allowedDomainsEnv
+                  ? allowedDomainsEnv.split(',').map(d => d.trim()).filter(d => d)
+                  : defaultAllowedDomains;
+                let sanitized = content;
+                // Neutralize @mentions to prevent unintended notifications
+                sanitized = neutralizeMentions(sanitized);
+                // Remove control characters (except newlines and tabs)
+                sanitized = sanitized.replace(/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/g, '');
+                // XML character escaping
+                sanitized = sanitized
+                  .replace(/&/g, '&amp;')   // Must be first to avoid double-escaping
+                  .replace(/</g, '&lt;')
+                  .replace(/>/g, '&gt;')
+                  .replace(/"/g, '&quot;')
+                  .replace(/'/g, '&apos;');
+                // URI filtering - replace non-https protocols with "(redacted)"
+                sanitized = sanitizeUrlProtocols(sanitized);
+                // Domain filtering for HTTPS URIs
+                sanitized = sanitizeUrlDomains(sanitized);
+                // Limit total length to prevent DoS (0.5MB max)
+                const maxLength = 524288;
+                if (sanitized.length > maxLength) {
+                  sanitized = sanitized.substring(0, maxLength) + '\n[Content truncated due to length]';
+                }
+                // Limit number of lines to prevent log flooding (65k max)
+                const lines = sanitized.split('\n');
+                const maxLines = 65000;
+                if (lines.length > maxLines) {
+                  sanitized = lines.slice(0, maxLines).join('\n') + '\n[Content truncated due to line count]';
+                }
+                // Remove ANSI escape sequences
+                sanitized = sanitized.replace(/\x1b\[[0-9;]*[mGKH]/g, '');
+                // Neutralize common bot trigger phrases
+                sanitized = neutralizeBotTriggers(sanitized);
+                // Trim excessive whitespace
+                return sanitized.trim();
+                /**
+                 * Remove unknown domains
+                 * @param {string} s - The string to process
+                 * @returns {string} The string with unknown domains redacted
+                 */
+                function sanitizeUrlDomains(s) {
+                  return s.replace(/\bhttps:\/\/([^\/\s\])}'"<>&\x00-\x1f]+)/gi, (match, domain) => {
+                    // Extract the hostname part (before first slash, colon, or other delimiter)
+                    const hostname = domain.split(/[\/:\?#]/)[0].toLowerCase();
+                    // Check if this domain or any parent domain is in the allowlist
+                    const isAllowed = allowedDomains.some(allowedDomain => {
+                      const normalizedAllowed = allowedDomain.toLowerCase();
+                      return hostname === normalizedAllowed || hostname.endsWith('.' + normalizedAllowed);
+                    });
+                    return isAllowed ? match : '(redacted)';
                   });
-                  return isAllowed ? match : '(redacted)';
-                });
-              }
-              /**
-               * Remove unknown protocols except https
-               * @param {string} s - The string to process
-               * @returns {string} The string with non-https protocols redacted
-               */
-              function sanitizeUrlProtocols(s) {
-                // Match both protocol:// and protocol: patterns
-                return s.replace(/\b(\w+):(?:\/\/)?[^\s\])}'"<>&\x00-\x1f]+/gi, (match, protocol) => {
-                  // Allow https (case insensitive), redact everything else
-                  return protocol.toLowerCase() === 'https' ? match : '(redacted)';
-                });
-              }
-              /**
-               * Neutralizes @mentions by wrapping them in backticks
-               * @param {string} s - The string to process
-               * @returns {string} The string with neutralized mentions
-               */
-              function neutralizeMentions(s) {
-                // Replace @name or @org/team outside code with `@name`
-                return s.replace(/(^|[^\w`])@([A-Za-z0-9](?:[A-Za-z0-9-]{0,37}[A-Za-z0-9])?(?:\/[A-Za-z0-9._-]+)?)/g,
-                  (_m, p1, p2) => `${p1}\`@${p2}\``);
+                }
+                /**
+                 * Remove unknown protocols except https
+                 * @param {string} s - The string to process
+                 * @returns {string} The string with non-https protocols redacted
+                 */
+                function sanitizeUrlProtocols(s) {
+                  // Match both protocol:// and protocol: patterns
+                  return s.replace(/\b(\w+):(?:\/\/)?[^\s\])}'"<>&\x00-\x1f]+/gi, (match, protocol) => {
+                    // Allow https (case insensitive), redact everything else
+                    return protocol.toLowerCase() === 'https' ? match : '(redacted)';
+                  });
+                }
+                /**
+                 * Neutralizes @mentions by wrapping them in backticks
+                 * @param {string} s - The string to process
+                 * @returns {string} The string with neutralized mentions
+                 */
+                function neutralizeMentions(s) {
+                  // Replace @name or @org/team outside code with `@name`
+                  return s.replace(/(^|[^\w`])@([A-Za-z0-9](?:[A-Za-z0-9-]{0,37}[A-Za-z0-9])?(?:\/[A-Za-z0-9._-]+)?)/g,
+                    (_m, p1, p2) => `${p1}\`@${p2}\``);
+                }
+                /**
+                 * Neutralizes bot trigger phrases by wrapping them in backticks
+                 * @param {string} s - The string to process
+                 * @returns {string} The string with neutralized bot triggers
+                 */
+                function neutralizeBotTriggers(s) {
+                  // Neutralize common bot trigger phrases like "fixes #123", "closes #asdfs", etc.
+                  return s.replace(/\b(fixes?|closes?|resolves?|fix|close|resolve)\s+#(\w+)/gi,
+                    (match, action, ref) => `\`${action} #${ref}\``);
+                }
               }
               /**
-               * Neutralizes bot trigger phrases by wrapping them in backticks
-               * @param {string} s - The string to process
-               * @returns {string} The string with neutralized bot triggers
+               * Gets the maximum allowed count for a given output type
+               * @param {string} itemType - The output item type
+               * @param {Object} config - The safe-outputs configuration
+               * @returns {number} The maximum allowed count
                */
-              function neutralizeBotTriggers(s) {
-                // Neutralize common bot trigger phrases like "fixes #123", "closes #asdfs", etc.
-                return s.replace(/\b(fixes?|closes?|resolves?|fix|close|resolve)\s+#(\w+)/gi,
-                  (match, action, ref) => `\`${action} #${ref}\``);
+              function getMaxAllowedForType(itemType, config) {
+                // Check if max is explicitly specified in config
+                if (config && config[itemType] && typeof config[itemType] === 'object' && config[itemType].max) {
+                  return config[itemType].max;
+                }
+                // Use default limits for plural-supported types
+                switch (itemType) {
+                  case 'create-issue':
+                    return 10; // Allow multiple issues
+                  case 'add-issue-comment':
+                    return 10; // Allow multiple comments
+                  case 'create-pull-request':
+                    return 1;  // Only one pull request allowed
+                  case 'add-issue-labels':
+                    return 1;  // Only one labels operation allowed
+                  default:
+                    return 1;  // Default to single item for unknown types
+                }
               }
-            }
-            async function main() {
-              const fs = require("fs");
               const outputFile = process.env.GITHUB_AW_SAFE_OUTPUTS;
               const safeOutputsConfig = process.env.GITHUB_AW_SAFE_OUTPUTS_CONFIG;
               if (!outputFile) {
@@ -541,10 +566,11 @@ jobs:
                     errors.push(`Line ${i + 1}: Unexpected output type '${itemType}'. Expected one of: ${Object.keys(expectedOutputTypes).join(', ')}`);
                     continue;
                   }
-                  // Check for duplicates based on type (since current format allows only one of each)
-                  const existingItem = parsedItems.find(existing => existing.type === itemType);
-                  if (existingItem) {
-                    errors.push(`Line ${i + 1}: Duplicate output type '${itemType}'. Only one item of each type is currently allowed.`);
+                  // Check for too many items of the same type
+                  const typeCount = parsedItems.filter(existing => existing.type === itemType).length;
+                  const maxAllowed = getMaxAllowedForType(itemType, expectedOutputTypes);
+                  if (typeCount >= maxAllowed) {
+                    errors.push(`Line ${i + 1}: Too many items of type '${itemType}'. Maximum allowed: ${maxAllowed}.`);
                     continue;
                   }
                   // Basic validation based on type
@@ -989,83 +1015,101 @@ jobs:
                 console.log('No valid items found in agent output');
                 return;
               }
-              // Find the create-issue item
-              const createIssueItem = validatedOutput.items.find(/** @param {any} item */ item => item.type === 'create-issue');
-              if (!createIssueItem) {
-                console.log('No create-issue item found in agent output');
+              // Find all create-issue items
+              const createIssueItems = validatedOutput.items.filter(/** @param {any} item */ item => item.type === 'create-issue');
+              if (createIssueItems.length === 0) {
+                console.log('No create-issue items found in agent output');
                 return;
               }
-              console.log('Found create-issue item:', { title: createIssueItem.title, bodyLength: createIssueItem.body.length });
+              console.log(`Found ${createIssueItems.length} create-issue item(s)`);
               // Check if we're in an issue context (triggered by an issue event)
               const parentIssueNumber = context.payload?.issue?.number;
-              // Parse labels from environment variable (comma-separated string) or from the JSON item
+              // Parse labels from environment variable (comma-separated string)
               const labelsEnv = process.env.GITHUB_AW_ISSUE_LABELS;
-              let labels = labelsEnv ? labelsEnv.split(',').map(/** @param {string} label */ label => label.trim()).filter(/** @param {string} label */ label => label) : [];
-              // If the item has labels, use those instead (or merge them)
-              if (createIssueItem.labels && Array.isArray(createIssueItem.labels)) {
-                labels = [...labels, ...createIssueItem.labels].filter(Boolean);
-              }
-              // Extract title and body from the JSON item
-              let title = createIssueItem.title ? createIssueItem.title.trim() : '';
-              let bodyLines = createIssueItem.body.split('\n');
-              // If no title was found, use the body content as title (or a default)
-              if (!title) {
-                title = createIssueItem.body || 'Agent Output';
-              }
-              // Apply title prefix if provided via environment variable
-              const titlePrefix = process.env.GITHUB_AW_ISSUE_TITLE_PREFIX;
-              if (titlePrefix && !title.startsWith(titlePrefix)) {
-                title = titlePrefix + title;
-              }
-              if (parentIssueNumber) {
-                console.log('Detected issue context, parent issue #' + parentIssueNumber);
-                // Add reference to parent issue in the child issue body
-                bodyLines.push(`Related to #${parentIssueNumber}`);
-              }
-              // Add AI disclaimer with run id, run htmlurl
-              // Add AI disclaimer with workflow run information
-              const runId = context.runId;
-              const runUrl = context.payload.repository 
-                ? `${context.payload.repository.html_url}/actions/runs/${runId}`
-                : `https://github.com/actions/runs/${runId}`;  
-              bodyLines.push(``, ``, `> Generated by Agentic Workflow Run [${runId}](${runUrl})`, '');
-              // Prepare the body content
-              const body = bodyLines.join('\n').trim();
-              console.log('Creating issue with title:', title);
-              console.log('Labels:', labels);
-              console.log('Body length:', body.length);
-              // Create the issue using GitHub API
-              const { data: issue } = await github.rest.issues.create({
-                owner: context.repo.owner,
-                repo: context.repo.repo,
-                title: title,
-                body: body,
-                labels: labels
-              });
-              console.log('Created issue #' + issue.number + ': ' + issue.html_url);
-              // If we have a parent issue, add a comment to it referencing the new child issue
-              if (parentIssueNumber) {
+              let envLabels = labelsEnv ? labelsEnv.split(',').map(/** @param {string} label */ label => label.trim()).filter(/** @param {string} label */ label => label) : [];
+              const createdIssues = [];
+              // Process each create-issue item
+              for (let i = 0; i < createIssueItems.length; i++) {
+                const createIssueItem = createIssueItems[i];
+                console.log(`Processing create-issue item ${i + 1}/${createIssueItems.length}:`, { title: createIssueItem.title, bodyLength: createIssueItem.body.length });
+                // Merge environment labels with item-specific labels
+                let labels = [...envLabels];
+                if (createIssueItem.labels && Array.isArray(createIssueItem.labels)) {
+                  labels = [...labels, ...createIssueItem.labels].filter(Boolean);
+                }
+                // Extract title and body from the JSON item
+                let title = createIssueItem.title ? createIssueItem.title.trim() : '';
+                let bodyLines = createIssueItem.body.split('\n');
+                // If no title was found, use the body content as title (or a default)
+                if (!title) {
+                  title = createIssueItem.body || 'Agent Output';
+                }
+                // Apply title prefix if provided via environment variable
+                const titlePrefix = process.env.GITHUB_AW_ISSUE_TITLE_PREFIX;
+                if (titlePrefix && !title.startsWith(titlePrefix)) {
+                  title = titlePrefix + title;
+                }
+                if (parentIssueNumber) {
+                  console.log('Detected issue context, parent issue #' + parentIssueNumber);
+                  // Add reference to parent issue in the child issue body
+                  bodyLines.push(`Related to #${parentIssueNumber}`);
+                }
+                // Add AI disclaimer with run id, run htmlurl
+                // Add AI disclaimer with workflow run information
+                const runId = context.runId;
+                const runUrl = context.payload.repository 
+                  ? `${context.payload.repository.html_url}/actions/runs/${runId}`
+                  : `https://github.com/actions/runs/${runId}`;  
+                bodyLines.push(``, ``, `> Generated by Agentic Workflow Run [${runId}](${runUrl})`, '');
+                // Prepare the body content
+                const body = bodyLines.join('\n').trim();
+                console.log('Creating issue with title:', title);
+                console.log('Labels:', labels);
+                console.log('Body length:', body.length);
                 try {
-                  await github.rest.issues.createComment({
+                  // Create the issue using GitHub API
+                  const { data: issue } = await github.rest.issues.create({
                     owner: context.repo.owner,
                     repo: context.repo.repo,
-                    issue_number: parentIssueNumber,
-                    body: `Created related issue: #${issue.number}`
+                    title: title,
+                    body: body,
+                    labels: labels
                   });
-                  console.log('Added comment to parent issue #' + parentIssueNumber);
+                  console.log('Created issue #' + issue.number + ': ' + issue.html_url);
+                  createdIssues.push(issue);
+                  // If we have a parent issue, add a comment to it referencing the new child issue
+                  if (parentIssueNumber) {
+                    try {
+                      await github.rest.issues.createComment({
+                        owner: context.repo.owner,
+                        repo: context.repo.repo,
+                        issue_number: parentIssueNumber,
+                        body: `Created related issue: #${issue.number}`
+                      });
+                      console.log('Added comment to parent issue #' + parentIssueNumber);
+                    } catch (error) {
+                      console.log('Warning: Could not add comment to parent issue:', error instanceof Error ? error.message : String(error));
+                    }
+                  }
+                  // Set output for the last created issue (for backward compatibility)
+                  if (i === createIssueItems.length - 1) {
+                    core.setOutput('issue_number', issue.number);
+                    core.setOutput('issue_url', issue.html_url);
+                  }
                 } catch (error) {
-                  console.log('Warning: Could not add comment to parent issue:', error instanceof Error ? error.message : String(error));
+                  console.error(`✗ Failed to create issue "${title}":`, error instanceof Error ? error.message : String(error));
+                  throw error;
+                }
+              }
+              // Write summary for all created issues
+              if (createdIssues.length > 0) {
+                let summaryContent = '\n\n## GitHub Issues\n';
+                for (const issue of createdIssues) {
+                  summaryContent += `- Issue #${issue.number}: [${issue.title}](${issue.html_url})\n`;
                 }
+                await core.summary.addRaw(summaryContent).write();
               }
-              // Set output for other jobs to use
-              core.setOutput('issue_number', issue.number);
-              core.setOutput('issue_url', issue.html_url);
-              // write issue to summary
-              await core.summary.addRaw(`
-            ## GitHub Issue
-            - Issue ID: ${issue.number}
-            - Issue URL: ${issue.html_url}
-            `).write();
+              console.log(`Successfully created ${createdIssues.length} issue(s)`);
             }
             await main();
 
diff --git a/.github/workflows/test-proxy.lock.yml b/.github/workflows/test-proxy.lock.yml
index e9c291dd2cc..24bb70fda7c 100644
--- a/.github/workflows/test-proxy.lock.yml
+++ b/.github/workflows/test-proxy.lock.yml
@@ -428,113 +428,138 @@ jobs:
           GITHUB_AW_SAFE_OUTPUTS_CONFIG: "{\"add-issue-comment\":true}"
         with:
           script: |
-            /**
-             * Sanitizes content for safe output in GitHub Actions
-             * @param {string} content - The content to sanitize
-             * @returns {string} The sanitized content
-             */
-            function sanitizeContent(content) {
-              if (!content || typeof content !== 'string') {
-                return '';
-              }
-              // Read allowed domains from environment variable
-              const allowedDomainsEnv = process.env.GITHUB_AW_ALLOWED_DOMAINS;
-              const defaultAllowedDomains = [
-                'github.com',
-                'github.io',
-                'githubusercontent.com',
-                'githubassets.com',
-                'github.dev',
-                'codespaces.new'
-              ];
-              const allowedDomains = allowedDomainsEnv
-                ? allowedDomainsEnv.split(',').map(d => d.trim()).filter(d => d)
-                : defaultAllowedDomains;
-              let sanitized = content;
-              // Neutralize @mentions to prevent unintended notifications
-              sanitized = neutralizeMentions(sanitized);
-              // Remove control characters (except newlines and tabs)
-              sanitized = sanitized.replace(/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/g, '');
-              // XML character escaping
-              sanitized = sanitized
-                .replace(/&/g, '&amp;')   // Must be first to avoid double-escaping
-                .replace(/</g, '&lt;')
-                .replace(/>/g, '&gt;')
-                .replace(/"/g, '&quot;')
-                .replace(/'/g, '&apos;');
-              // URI filtering - replace non-https protocols with "(redacted)"
-              sanitized = sanitizeUrlProtocols(sanitized);
-              // Domain filtering for HTTPS URIs
-              sanitized = sanitizeUrlDomains(sanitized);
-              // Limit total length to prevent DoS (0.5MB max)
-              const maxLength = 524288;
-              if (sanitized.length > maxLength) {
-                sanitized = sanitized.substring(0, maxLength) + '\n[Content truncated due to length]';
-              }
-              // Limit number of lines to prevent log flooding (65k max)
-              const lines = sanitized.split('\n');
-              const maxLines = 65000;
-              if (lines.length > maxLines) {
-                sanitized = lines.slice(0, maxLines).join('\n') + '\n[Content truncated due to line count]';
-              }
-              // Remove ANSI escape sequences
-              sanitized = sanitized.replace(/\x1b\[[0-9;]*[mGKH]/g, '');
-              // Neutralize common bot trigger phrases
-              sanitized = neutralizeBotTriggers(sanitized);
-              // Trim excessive whitespace
-              return sanitized.trim();
+            async function main() {
+              const fs = require("fs");
               /**
-               * Remove unknown domains
-               * @param {string} s - The string to process
-               * @returns {string} The string with unknown domains redacted
+               * Sanitizes content for safe output in GitHub Actions
+               * @param {string} content - The content to sanitize
+               * @returns {string} The sanitized content
                */
-              function sanitizeUrlDomains(s) {
-                return s.replace(/\bhttps:\/\/([^\/\s\])}'"<>&\x00-\x1f]+)/gi, (match, domain) => {
-                  // Extract the hostname part (before first slash, colon, or other delimiter)
-                  const hostname = domain.split(/[\/:\?#]/)[0].toLowerCase();
-                  // Check if this domain or any parent domain is in the allowlist
-                  const isAllowed = allowedDomains.some(allowedDomain => {
-                    const normalizedAllowed = allowedDomain.toLowerCase();
-                    return hostname === normalizedAllowed || hostname.endsWith('.' + normalizedAllowed);
+              function sanitizeContent(content) {
+                if (!content || typeof content !== 'string') {
+                  return '';
+                }
+                // Read allowed domains from environment variable
+                const allowedDomainsEnv = process.env.GITHUB_AW_ALLOWED_DOMAINS;
+                const defaultAllowedDomains = [
+                  'github.com',
+                  'github.io',
+                  'githubusercontent.com',
+                  'githubassets.com',
+                  'github.dev',
+                  'codespaces.new'
+                ];
+                const allowedDomains = allowedDomainsEnv
+                  ? allowedDomainsEnv.split(',').map(d => d.trim()).filter(d => d)
+                  : defaultAllowedDomains;
+                let sanitized = content;
+                // Neutralize @mentions to prevent unintended notifications
+                sanitized = neutralizeMentions(sanitized);
+                // Remove control characters (except newlines and tabs)
+                sanitized = sanitized.replace(/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/g, '');
+                // XML character escaping
+                sanitized = sanitized
+                  .replace(/&/g, '&amp;')   // Must be first to avoid double-escaping
+                  .replace(/</g, '&lt;')
+                  .replace(/>/g, '&gt;')
+                  .replace(/"/g, '&quot;')
+                  .replace(/'/g, '&apos;');
+                // URI filtering - replace non-https protocols with "(redacted)"
+                sanitized = sanitizeUrlProtocols(sanitized);
+                // Domain filtering for HTTPS URIs
+                sanitized = sanitizeUrlDomains(sanitized);
+                // Limit total length to prevent DoS (0.5MB max)
+                const maxLength = 524288;
+                if (sanitized.length > maxLength) {
+                  sanitized = sanitized.substring(0, maxLength) + '\n[Content truncated due to length]';
+                }
+                // Limit number of lines to prevent log flooding (65k max)
+                const lines = sanitized.split('\n');
+                const maxLines = 65000;
+                if (lines.length > maxLines) {
+                  sanitized = lines.slice(0, maxLines).join('\n') + '\n[Content truncated due to line count]';
+                }
+                // Remove ANSI escape sequences
+                sanitized = sanitized.replace(/\x1b\[[0-9;]*[mGKH]/g, '');
+                // Neutralize common bot trigger phrases
+                sanitized = neutralizeBotTriggers(sanitized);
+                // Trim excessive whitespace
+                return sanitized.trim();
+                /**
+                 * Remove unknown domains
+                 * @param {string} s - The string to process
+                 * @returns {string} The string with unknown domains redacted
+                 */
+                function sanitizeUrlDomains(s) {
+                  return s.replace(/\bhttps:\/\/([^\/\s\])}'"<>&\x00-\x1f]+)/gi, (match, domain) => {
+                    // Extract the hostname part (before first slash, colon, or other delimiter)
+                    const hostname = domain.split(/[\/:\?#]/)[0].toLowerCase();
+                    // Check if this domain or any parent domain is in the allowlist
+                    const isAllowed = allowedDomains.some(allowedDomain => {
+                      const normalizedAllowed = allowedDomain.toLowerCase();
+                      return hostname === normalizedAllowed || hostname.endsWith('.' + normalizedAllowed);
+                    });
+                    return isAllowed ? match : '(redacted)';
                   });
-                  return isAllowed ? match : '(redacted)';
-                });
-              }
-              /**
-               * Remove unknown protocols except https
-               * @param {string} s - The string to process
-               * @returns {string} The string with non-https protocols redacted
-               */
-              function sanitizeUrlProtocols(s) {
-                // Match both protocol:// and protocol: patterns
-                return s.replace(/\b(\w+):(?:\/\/)?[^\s\])}'"<>&\x00-\x1f]+/gi, (match, protocol) => {
-                  // Allow https (case insensitive), redact everything else
-                  return protocol.toLowerCase() === 'https' ? match : '(redacted)';
-                });
-              }
-              /**
-               * Neutralizes @mentions by wrapping them in backticks
-               * @param {string} s - The string to process
-               * @returns {string} The string with neutralized mentions
-               */
-              function neutralizeMentions(s) {
-                // Replace @name or @org/team outside code with `@name`
-                return s.replace(/(^|[^\w`])@([A-Za-z0-9](?:[A-Za-z0-9-]{0,37}[A-Za-z0-9])?(?:\/[A-Za-z0-9._-]+)?)/g,
-                  (_m, p1, p2) => `${p1}\`@${p2}\``);
+                }
+                /**
+                 * Remove unknown protocols except https
+                 * @param {string} s - The string to process
+                 * @returns {string} The string with non-https protocols redacted
+                 */
+                function sanitizeUrlProtocols(s) {
+                  // Match both protocol:// and protocol: patterns
+                  return s.replace(/\b(\w+):(?:\/\/)?[^\s\])}'"<>&\x00-\x1f]+/gi, (match, protocol) => {
+                    // Allow https (case insensitive), redact everything else
+                    return protocol.toLowerCase() === 'https' ? match : '(redacted)';
+                  });
+                }
+                /**
+                 * Neutralizes @mentions by wrapping them in backticks
+                 * @param {string} s - The string to process
+                 * @returns {string} The string with neutralized mentions
+                 */
+                function neutralizeMentions(s) {
+                  // Replace @name or @org/team outside code with `@name`
+                  return s.replace(/(^|[^\w`])@([A-Za-z0-9](?:[A-Za-z0-9-]{0,37}[A-Za-z0-9])?(?:\/[A-Za-z0-9._-]+)?)/g,
+                    (_m, p1, p2) => `${p1}\`@${p2}\``);
+                }
+                /**
+                 * Neutralizes bot trigger phrases by wrapping them in backticks
+                 * @param {string} s - The string to process
+                 * @returns {string} The string with neutralized bot triggers
+                 */
+                function neutralizeBotTriggers(s) {
+                  // Neutralize common bot trigger phrases like "fixes #123", "closes #asdfs", etc.
+                  return s.replace(/\b(fixes?|closes?|resolves?|fix|close|resolve)\s+#(\w+)/gi,
+                    (match, action, ref) => `\`${action} #${ref}\``);
+                }
               }
               /**
-               * Neutralizes bot trigger phrases by wrapping them in backticks
-               * @param {string} s - The string to process
-               * @returns {string} The string with neutralized bot triggers
+               * Gets the maximum allowed count for a given output type
+               * @param {string} itemType - The output item type
+               * @param {Object} config - The safe-outputs configuration
+               * @returns {number} The maximum allowed count
                */
-              function neutralizeBotTriggers(s) {
-                // Neutralize common bot trigger phrases like "fixes #123", "closes #asdfs", etc.
-                return s.replace(/\b(fixes?|closes?|resolves?|fix|close|resolve)\s+#(\w+)/gi,
-                  (match, action, ref) => `\`${action} #${ref}\``);
+              function getMaxAllowedForType(itemType, config) {
+                // Check if max is explicitly specified in config
+                if (config && config[itemType] && typeof config[itemType] === 'object' && config[itemType].max) {
+                  return config[itemType].max;
+                }
+                // Use default limits for plural-supported types
+                switch (itemType) {
+                  case 'create-issue':
+                    return 10; // Allow multiple issues
+                  case 'add-issue-comment':
+                    return 10; // Allow multiple comments
+                  case 'create-pull-request':
+                    return 1;  // Only one pull request allowed
+                  case 'add-issue-labels':
+                    return 1;  // Only one labels operation allowed
+                  default:
+                    return 1;  // Default to single item for unknown types
+                }
               }
-            }
-            async function main() {
-              const fs = require("fs");
               const outputFile = process.env.GITHUB_AW_SAFE_OUTPUTS;
               const safeOutputsConfig = process.env.GITHUB_AW_SAFE_OUTPUTS_CONFIG;
               if (!outputFile) {
@@ -584,10 +609,11 @@ jobs:
                     errors.push(`Line ${i + 1}: Unexpected output type '${itemType}'. Expected one of: ${Object.keys(expectedOutputTypes).join(', ')}`);
                     continue;
                   }
-                  // Check for duplicates based on type (since current format allows only one of each)
-                  const existingItem = parsedItems.find(existing => existing.type === itemType);
-                  if (existingItem) {
-                    errors.push(`Line ${i + 1}: Duplicate output type '${itemType}'. Only one item of each type is currently allowed.`);
+                  // Check for too many items of the same type
+                  const typeCount = parsedItems.filter(existing => existing.type === itemType).length;
+                  const maxAllowed = getMaxAllowedForType(itemType, expectedOutputTypes);
+                  if (typeCount >= maxAllowed) {
+                    errors.push(`Line ${i + 1}: Too many items of type '${itemType}'. Maximum allowed: ${maxAllowed}.`);
                     continue;
                   }
                   // Basic validation based on type
@@ -1089,13 +1115,13 @@ jobs:
                 console.log('No valid items found in agent output');
                 return;
               }
-              // Find the add-issue-comment item
-              const commentItem = validatedOutput.items.find(/** @param {any} item */ item => item.type === 'add-issue-comment');
-              if (!commentItem) {
-                console.log('No add-issue-comment item found in agent output');
+              // Find all add-issue-comment items
+              const commentItems = validatedOutput.items.filter(/** @param {any} item */ item => item.type === 'add-issue-comment');
+              if (commentItems.length === 0) {
+                console.log('No add-issue-comment items found in agent output');
                 return;
               }
-              console.log('Found add-issue-comment item:', { bodyLength: commentItem.body.length });
+              console.log(`Found ${commentItems.length} add-issue-comment item(s)`);
               // Check if we're in an issue or pull request context
               const isIssueContext = context.eventName === 'issues' || context.eventName === 'issue_comment';
               const isPRContext = context.eventName === 'pull_request' || context.eventName === 'pull_request_review' || context.eventName === 'pull_request_review_comment';
@@ -1103,57 +1129,75 @@ jobs:
                 console.log('Not running in issue or pull request context, skipping comment creation');
                 return;
               }
-              // Determine the issue/PR number and comment endpoint
-              let issueNumber;
-              let commentEndpoint;
-              if (isIssueContext) {
-                if (context.payload.issue) {
-                  issueNumber = context.payload.issue.number;
-                  commentEndpoint = 'issues';
-                } else {
-                  console.log('Issue context detected but no issue found in payload');
-                  return;
+              const createdComments = [];
+              // Process each comment item
+              for (let i = 0; i < commentItems.length; i++) {
+                const commentItem = commentItems[i];
+                console.log(`Processing add-issue-comment item ${i + 1}/${commentItems.length}:`, { bodyLength: commentItem.body.length });
+                // Determine the issue/PR number and comment endpoint for this comment
+                let issueNumber;
+                let commentEndpoint;
+                if (isIssueContext) {
+                  if (context.payload.issue) {
+                    issueNumber = context.payload.issue.number;
+                    commentEndpoint = 'issues';
+                  } else {
+                    console.log('Issue context detected but no issue found in payload');
+                    continue;
+                  }
+                } else if (isPRContext) {
+                  if (context.payload.pull_request) {
+                    issueNumber = context.payload.pull_request.number;
+                    commentEndpoint = 'issues'; // PR comments use the issues API endpoint
+                  } else {
+                    console.log('Pull request context detected but no pull request found in payload');
+                    continue;
+                  }
                 }
-              } else if (isPRContext) {
-                if (context.payload.pull_request) {
-                  issueNumber = context.payload.pull_request.number;
-                  commentEndpoint = 'issues'; // PR comments use the issues API endpoint
-                } else {
-                  console.log('Pull request context detected but no pull request found in payload');
-                  return;
+                if (!issueNumber) {
+                  console.log('Could not determine issue or pull request number');
+                  continue;
+                }
+                // Extract body from the JSON item
+                let body = commentItem.body.trim();
+                // Add AI disclaimer with run id, run htmlurl
+                const runId = context.runId;
+                const runUrl = context.payload.repository 
+                  ? `${context.payload.repository.html_url}/actions/runs/${runId}`
+                  : `https://github.com/actions/runs/${runId}`;  
+                body += `\n\n> Generated by Agentic Workflow Run [${runId}](${runUrl})\n`;
+                console.log(`Creating comment on ${commentEndpoint} #${issueNumber}`);
+                console.log('Comment content length:', body.length);
+                try {
+                  // Create the comment using GitHub API
+                  const { data: comment } = await github.rest.issues.createComment({
+                    owner: context.repo.owner,
+                    repo: context.repo.repo,
+                    issue_number: issueNumber,
+                    body: body
+                  });
+                  console.log('Created comment #' + comment.id + ': ' + comment.html_url);
+                  createdComments.push(comment);
+                  // Set output for the last created comment (for backward compatibility)
+                  if (i === commentItems.length - 1) {
+                    core.setOutput('comment_id', comment.id);
+                    core.setOutput('comment_url', comment.html_url);
+                  }
+                } catch (error) {
+                  console.error(`✗ Failed to create comment:`, error instanceof Error ? error.message : String(error));
+                  throw error;
                 }
               }
-              if (!issueNumber) {
-                console.log('Could not determine issue or pull request number');
-                return;
+              // Write summary for all created comments
+              if (createdComments.length > 0) {
+                let summaryContent = '\n\n## GitHub Comments\n';
+                for (const comment of createdComments) {
+                  summaryContent += `- Comment #${comment.id}: [View Comment](${comment.html_url})\n`;
+                }
+                await core.summary.addRaw(summaryContent).write();
               }
-              // Extract body from the JSON item
-              let body = commentItem.body.trim();
-              // Add AI disclaimer with run id, run htmlurl
-              const runId = context.runId;
-              const runUrl = context.payload.repository 
-                ? `${context.payload.repository.html_url}/actions/runs/${runId}`
-                : `https://github.com/actions/runs/${runId}`;  
-              body += `\n\n> Generated by Agentic Workflow Run [${runId}](${runUrl})\n`;
-              console.log(`Creating comment on ${commentEndpoint} #${issueNumber}`);
-              console.log('Comment content length:', body.length);
-              // Create the comment using GitHub API
-              const { data: comment } = await github.rest.issues.createComment({
-                owner: context.repo.owner,
-                repo: context.repo.repo,
-                issue_number: issueNumber,
-                body: body
-              });
-              console.log('Created comment #' + comment.id + ': ' + comment.html_url);
-              // Set output for other jobs to use
-              core.setOutput('comment_id', comment.id);
-              core.setOutput('comment_url', comment.html_url);
-              // write comment id, url to the github_step_summary
-              await core.summary.addRaw(`
-            ## GitHub Comment
-            - Comment ID: ${comment.id}
-            - Comment URL: ${comment.html_url}
-            `).write();
+              console.log(`Successfully created ${createdComments.length} comment(s)`);
+              return createdComments;
             }
             await main();
 
diff --git a/docs/safe-outputs.md b/docs/safe-outputs.md
index 0b07134a9b1..1a44242ec1e 100644
--- a/docs/safe-outputs.md
+++ b/docs/safe-outputs.md
@@ -13,47 +13,62 @@ The `safe-outputs:` element of your workflow's frontmatter declares that your ag
 
 ## Available Output Types
 
-### New Issue Creation (`create-issue:`)
+### New Issue Creation (`create-issue:` / `create-issues:`)
 
-Adding `create-issue:` to the `safe-outputs:` section of your workflow declares that the workflow should conclude with the creation of a GitHub issue based on the workflow's output.
+Adding issue creation to the `safe-outputs:` section declares that the workflow should conclude with the creation of GitHub issues based on the workflow's output.
 
+**Singular Form (create exactly one issue):**
 ```yaml
 safe-outputs:
   create-issue:
 ```
 
-or with further configuration:
+**Plural Form (create multiple issues):**
+```yaml
+safe-outputs:
+  create-issues:          
+    max: 5                          # Optional: maximum number of issues (default: 10)
+```
 
+**With Configuration:**
 ```yaml
 safe-outputs:
-  create-issue:
-    title-prefix: "[ai] "           # Optional: prefix for issue titles
-    labels: [automation, agent]     # Optional: labels to attach to issues
+  create-issue:                      # Singular form
+    title-prefix: "[ai] "            # Optional: prefix for issue titles
+    labels: [automation, agent]      # Optional: labels to attach to issues
 ```
 
-The agentic part of your workflow should describe the issue it wants created.
+The agentic part of your workflow should describe the issue(s) it wants created.
 
-**Example natural language to generate the output:**
+**Example markdown to generate the output:**
 
 ```yaml
 # Code Analysis Agent
 
 Analyze the latest commit and provide insights.
-Create a new issue with your findings with title "AI Code Analysis" and description "Here are the details of the analysis..."
+Create new issues with your findings. For each issue, provide a title starting with "AI Code Analysis" and detailed description of the analysis findings.
 ```
 
-The workflow will have additional prompting describing that, to create the issue, the agent should write the issue title and body to a file.
+The workflow will have additional prompting describing that, to create issues, the agent should write the issue details to a file.
 
-### Issue Comment Creation (`add-issue-comment:`)
+### Issue Comment Creation (`add-issue-comment:` / `add-issue-comments:`)
 
-Adding `add-issue-comment:` to the `safe-outputs:` section of your workflow declares that the workflow should conclude with posting a comment on the triggering issue or pull request based on the workflow's output.
+Adding comment creation to the `safe-outputs:` section declares that the workflow should conclude with posting comments on the triggering issue or pull request based on the workflow's output.
 
+**Singular Form (adds exactly one comment):**
 ```yaml
 safe-outputs:
   add-issue-comment:
 ```
 
-The agentic part of your workflow should describe the comment it wants posted.
+**Plural Form (adds multiple comments):**
+```yaml
+safe-outputs:
+  add-issue-comments:
+    max: 3                # Optional: maximum number of comments (default: 10)
+```
+
+The agentic part of your workflow should describe the comment(s) it wants posted.
 
 **Example natural language to generate the output:**
 
@@ -61,33 +76,34 @@ The agentic part of your workflow should describe the comment it wants posted.
 # Issue/PR Analysis Agent
 
 Analyze the issue or pull request and provide feedback.
-Create an issue comment on the triggering issue or PR starting with the text "Here is my analysis of the issue/PR..."
+Create issue comments on the triggering issue or PR with your analysis findings. Each comment should provide specific insights about different aspects of the issue.
 ```
 
-The workflow will have additional prompting describing that, to create the issue, the agent should write the comment body to a special file..
+The workflow will have additional prompting describing that, to create comments, the agent should write the comment content to a special file.
 
 ### Pull Request Creation (`create-pull-request:`)
 
-Adding `create-pull-request:` to the `safe-outputs:` section of your workflow declares that the workflow should conclude with the creation of a pull request containing code changes generated by the workflow.
+Adding pull request creation to the `safe-outputs:` section declares that the workflow should conclude with the creation of a pull request containing code changes generated by the workflow.
 
 ```yaml
 safe-outputs:
   create-pull-request:
 ```
 
-or with further configuration:
-
+**With Configuration:**
 ```yaml
 safe-outputs:
-  create-pull-request:
-    title-prefix: "[ai] "           # Optional: prefix for PR titles
-    labels: [automation, ai-agent]  # Optional: labels to attach to PRs
-    draft: true                     # Optional: create as draft PR (defaults to true)
+  create-pull-request:               # Creates exactly one pull request
+    title-prefix: "[ai] "            # Optional: prefix for PR titles
+    labels: [automation, ai-agent]   # Optional: labels to attach to PRs
+    draft: true                      # Optional: create as draft PR (defaults to true)
 ```
 
-The agentic part of your workflow should instruct to 
+At most one pull request is currently supported.
+
+The agentic part of your workflow should instruct to:
 1. **Make code changes**: Make any code changes in the working directory—these are automatically collected using `git add -A` and committed
-2. **Create a pull request**: Describe the pull request title and body you want
+2. **Create pull request**: Describe the pull request title and body content you want
 
 **Example natural language to generate the output:**
 
@@ -97,7 +113,7 @@ The agentic part of your workflow should instruct to
 Analyze the latest commit and suggest improvements.
 
 1. Make any file changes directly in the working directory
-2. Create a PR with title "First change" and description "THis is a first change"
+2. Create a pull request for your improvements, with a descriptive title and detailed description of the changes made
 ```
 
 ### Label Addition (`add-issue-labels:`)
diff --git a/pkg/cli/commands.go b/pkg/cli/commands.go
index 0f1ddf7e9fe..91a8251a1b0 100644
--- a/pkg/cli/commands.go
+++ b/pkg/cli/commands.go
@@ -3685,9 +3685,13 @@ permissions:
 
 # Outputs - what APIs and tools can the AI use?
 safe-outputs:
-  create-issue:
-  # create-pull-request:
-  # add-issue-comment:
+  create-issue:          # Creates exactly one issue
+  # create-issues:       # Creates multiple issues (default max: 10)
+  #   max: 5             # Optional: specify maximum number
+  # create-pull-request: # Creates exactly one pull request
+  # add-issue-comment:   # Adds exactly one comment
+  # add-issue-comments:  # Adds multiple comments (default max: 10)
+  #   max: 2             # Optional: specify maximum number
   # add-issue-labels:
 
 ---
diff --git a/pkg/parser/schemas/main_workflow_schema.json b/pkg/parser/schemas/main_workflow_schema.json
index 10e47efc018..3298e95c05c 100644
--- a/pkg/parser/schemas/main_workflow_schema.json
+++ b/pkg/parser/schemas/main_workflow_schema.json
@@ -1009,6 +1009,38 @@
             }
           ]
         },
+        "create-issues": {
+          "oneOf": [
+            {
+              "type": "object",
+              "description": "Configuration for creating GitHub issues from agentic workflow output (plural form)",
+              "properties": {
+                "title-prefix": {
+                  "type": "string",
+                  "description": "Optional prefix for the issue title"
+                },
+                "labels": {
+                  "type": "array",
+                  "description": "Optional list of labels to attach to the issue",
+                  "items": {
+                    "type": "string"
+                  }
+                },
+                "max": {
+                  "type": "integer",
+                  "description": "Maximum number of issues to create (default: 10)",
+                  "minimum": 1,
+                  "maximum": 100
+                }
+              },
+              "additionalProperties": false
+            },
+            {
+              "type": "null",
+              "description": "Enable issue creation with default configuration (max: 10)"
+            }
+          ]
+        },
         "add-issue-comment": {
           "oneOf": [
             {
@@ -1022,6 +1054,27 @@
             }
           ]
         },
+        "add-issue-comments": {
+          "oneOf": [
+            {
+              "type": "object",
+              "description": "Configuration for creating GitHub issue/PR comments from agentic workflow output (plural form)",
+              "properties": {
+                "max": {
+                  "type": "integer",
+                  "description": "Maximum number of comments to create (default: 10)",
+                  "minimum": 1,
+                  "maximum": 100
+                }
+              },
+              "additionalProperties": false
+            },
+            {
+              "type": "null",
+              "description": "Enable issue comment creation with default configuration (max: 10)"
+            }
+          ]
+        },
         "create-pull-request": {
           "oneOf": [
             {
diff --git a/pkg/workflow/compiler.go b/pkg/workflow/compiler.go
index fa00d31ec01..59242753043 100644
--- a/pkg/workflow/compiler.go
+++ b/pkg/workflow/compiler.go
@@ -142,29 +142,36 @@ type WorkflowData struct {
 
 // SafeOutputsConfig holds configuration for automatic output routes
 type SafeOutputsConfig struct {
-	CreateIssue       *CreateIssueConfig       `yaml:"create-issue,omitempty"`
-	AddIssueComment   *AddIssueCommentConfig   `yaml:"add-issue-comment,omitempty"`
-	CreatePullRequest *CreatePullRequestConfig `yaml:"create-pull-request,omitempty"`
-	AddIssueLabels    *AddIssueLabelsConfig    `yaml:"add-issue-labels,omitempty"`
-	AllowedDomains    []string                 `yaml:"allowed-domains,omitempty"`
+	CreateIssues       *CreateIssuesConfig       `yaml:"create-issues,omitempty"`
+	AddIssueComments   *AddIssueCommentsConfig   `yaml:"add-issue-comments,omitempty"`
+	CreatePullRequests *CreatePullRequestsConfig `yaml:"create-pull-requests,omitempty"`
+	AddIssueLabels     *AddIssueLabelsConfig     `yaml:"add-issue-labels,omitempty"`
+	AllowedDomains     []string                  `yaml:"allowed-domains,omitempty"`
 }
 
-// CreateIssueConfig holds configuration for creating GitHub issues from agent output
-type CreateIssueConfig struct {
+// CreateIssuesConfig holds configuration for creating GitHub issues from agent output
+type CreateIssuesConfig struct {
 	TitlePrefix string   `yaml:"title-prefix,omitempty"`
 	Labels      []string `yaml:"labels,omitempty"`
+	Max         int      `yaml:"max,omitempty"` // Maximum number of issues to create
 }
 
-// AddIssueCommentConfig holds configuration for creating GitHub issue/PR comments from agent output
+// AddIssueCommentConfig holds configuration for creating GitHub issue/PR comments from agent output (deprecated, use AddIssueCommentsConfig)
 type AddIssueCommentConfig struct {
 	// Empty struct for now, as per requirements, but structured for future expansion
 }
 
-// CreatePullRequestConfig holds configuration for creating GitHub pull requests from agent output
-type CreatePullRequestConfig struct {
+// AddIssueCommentsConfig holds configuration for creating GitHub issue/PR comments from agent output
+type AddIssueCommentsConfig struct {
+	Max int `yaml:"max,omitempty"` // Maximum number of comments to create
+}
+
+// CreatePullRequestsConfig holds configuration for creating GitHub pull requests from agent output
+type CreatePullRequestsConfig struct {
 	TitlePrefix string   `yaml:"title-prefix,omitempty"`
 	Labels      []string `yaml:"labels,omitempty"`
 	Draft       *bool    `yaml:"draft,omitempty"` // Pointer to distinguish between unset (nil) and explicitly false
+	Max         int      `yaml:"max,omitempty"`   // Maximum number of pull requests to create
 }
 
 // AddIssueLabelsConfig holds configuration for adding labels to issues/PRs from agent output
@@ -1542,7 +1549,7 @@ func (c *Compiler) buildJobs(data *WorkflowData) error {
 
 	if data.SafeOutputs != nil {
 		// Build create_issue job if output.create_issue is configured
-		if data.SafeOutputs.CreateIssue != nil {
+		if data.SafeOutputs.CreateIssues != nil {
 			createIssueJob, err := c.buildCreateOutputIssueJob(data, jobName)
 			if err != nil {
 				return fmt.Errorf("failed to build create_issue job: %w", err)
@@ -1552,8 +1559,8 @@ func (c *Compiler) buildJobs(data *WorkflowData) error {
 			}
 		}
 
-		// Build create_issue_comment job if output.add-issue-comment is configured
-		if data.SafeOutputs.AddIssueComment != nil {
+		// Build create_issue_comment job if output.add-issue-comments is configured
+		if data.SafeOutputs.AddIssueComments != nil {
 			createCommentJob, err := c.buildCreateOutputAddIssueCommentJob(data, jobName)
 			if err != nil {
 				return fmt.Errorf("failed to build create_issue_comment job: %w", err)
@@ -1563,8 +1570,8 @@ func (c *Compiler) buildJobs(data *WorkflowData) error {
 			}
 		}
 
-		// Build create_pull_request job if output.create-pull-request is configured
-		if data.SafeOutputs.CreatePullRequest != nil {
+		// Build create_pull_request job if output.create-pull-requests is configured
+		if data.SafeOutputs.CreatePullRequests != nil {
 			createPullRequestJob, err := c.buildCreateOutputPullRequestJob(data, jobName)
 			if err != nil {
 				return fmt.Errorf("failed to build create_pull_request job: %w", err)
@@ -1716,8 +1723,8 @@ func (c *Compiler) buildAddReactionJob(data *WorkflowData, taskJobCreated bool)
 
 // buildCreateOutputIssueJob creates the create_issue job
 func (c *Compiler) buildCreateOutputIssueJob(data *WorkflowData, mainJobName string) (*Job, error) {
-	if data.SafeOutputs == nil || data.SafeOutputs.CreateIssue == nil {
-		return nil, fmt.Errorf("safe-outputs.create-issue configuration is required")
+	if data.SafeOutputs == nil || data.SafeOutputs.CreateIssues == nil {
+		return nil, fmt.Errorf("safe-outputs.create-issues configuration is required")
 	}
 
 	var steps []string
@@ -1729,11 +1736,11 @@ func (c *Compiler) buildCreateOutputIssueJob(data *WorkflowData, mainJobName str
 	steps = append(steps, "        env:\n")
 	// Pass the agent output content from the main job
 	steps = append(steps, fmt.Sprintf("          GITHUB_AW_AGENT_OUTPUT: ${{ needs.%s.outputs.output }}\n", mainJobName))
-	if data.SafeOutputs.CreateIssue.TitlePrefix != "" {
-		steps = append(steps, fmt.Sprintf("          GITHUB_AW_ISSUE_TITLE_PREFIX: %q\n", data.SafeOutputs.CreateIssue.TitlePrefix))
+	if data.SafeOutputs.CreateIssues.TitlePrefix != "" {
+		steps = append(steps, fmt.Sprintf("          GITHUB_AW_ISSUE_TITLE_PREFIX: %q\n", data.SafeOutputs.CreateIssues.TitlePrefix))
 	}
-	if len(data.SafeOutputs.CreateIssue.Labels) > 0 {
-		labelsStr := strings.Join(data.SafeOutputs.CreateIssue.Labels, ",")
+	if len(data.SafeOutputs.CreateIssues.Labels) > 0 {
+		labelsStr := strings.Join(data.SafeOutputs.CreateIssues.Labels, ",")
 		steps = append(steps, fmt.Sprintf("          GITHUB_AW_ISSUE_LABELS: %q\n", labelsStr))
 	}
 
@@ -1766,8 +1773,8 @@ func (c *Compiler) buildCreateOutputIssueJob(data *WorkflowData, mainJobName str
 
 // buildCreateOutputAddIssueCommentJob creates the create_issue_comment job
 func (c *Compiler) buildCreateOutputAddIssueCommentJob(data *WorkflowData, mainJobName string) (*Job, error) {
-	if data.SafeOutputs == nil || data.SafeOutputs.AddIssueComment == nil {
-		return nil, fmt.Errorf("safe-outputs.add-issue-comment configuration is required")
+	if data.SafeOutputs == nil || data.SafeOutputs.AddIssueComments == nil {
+		return nil, fmt.Errorf("safe-outputs.add-issue-comments configuration is required")
 	}
 
 	var steps []string
@@ -1809,8 +1816,8 @@ func (c *Compiler) buildCreateOutputAddIssueCommentJob(data *WorkflowData, mainJ
 
 // buildCreateOutputPullRequestJob creates the create_pull_request job
 func (c *Compiler) buildCreateOutputPullRequestJob(data *WorkflowData, mainJobName string) (*Job, error) {
-	if data.SafeOutputs == nil || data.SafeOutputs.CreatePullRequest == nil {
-		return nil, fmt.Errorf("safe-outputs.pull-request configuration is required")
+	if data.SafeOutputs == nil || data.SafeOutputs.CreatePullRequests == nil {
+		return nil, fmt.Errorf("safe-outputs.create-pull-requests configuration is required")
 	}
 
 	var steps []string
@@ -1841,17 +1848,17 @@ func (c *Compiler) buildCreateOutputPullRequestJob(data *WorkflowData, mainJobNa
 	steps = append(steps, fmt.Sprintf("          GITHUB_AW_WORKFLOW_ID: %q\n", mainJobName))
 	// Pass the base branch from GitHub context
 	steps = append(steps, "          GITHUB_AW_BASE_BRANCH: ${{ github.ref_name }}\n")
-	if data.SafeOutputs.CreatePullRequest.TitlePrefix != "" {
-		steps = append(steps, fmt.Sprintf("          GITHUB_AW_PR_TITLE_PREFIX: %q\n", data.SafeOutputs.CreatePullRequest.TitlePrefix))
+	if data.SafeOutputs.CreatePullRequests.TitlePrefix != "" {
+		steps = append(steps, fmt.Sprintf("          GITHUB_AW_PR_TITLE_PREFIX: %q\n", data.SafeOutputs.CreatePullRequests.TitlePrefix))
 	}
-	if len(data.SafeOutputs.CreatePullRequest.Labels) > 0 {
-		labelsStr := strings.Join(data.SafeOutputs.CreatePullRequest.Labels, ",")
+	if len(data.SafeOutputs.CreatePullRequests.Labels) > 0 {
+		labelsStr := strings.Join(data.SafeOutputs.CreatePullRequests.Labels, ",")
 		steps = append(steps, fmt.Sprintf("          GITHUB_AW_PR_LABELS: %q\n", labelsStr))
 	}
 	// Pass draft setting - default to true for backwards compatibility
 	draftValue := true // Default value
-	if data.SafeOutputs.CreatePullRequest.Draft != nil {
-		draftValue = *data.SafeOutputs.CreatePullRequest.Draft
+	if data.SafeOutputs.CreatePullRequests.Draft != nil {
+		draftValue = *data.SafeOutputs.CreatePullRequests.Draft
 	}
 	steps = append(steps, fmt.Sprintf("          GITHUB_AW_PR_DRAFT: %q\n", fmt.Sprintf("%t", draftValue)))
 
@@ -2265,17 +2272,17 @@ func (c *Compiler) generatePrompt(yaml *strings.Builder, data *WorkflowData, eng
 		yaml.WriteString("          \n")
 		yaml.WriteString("          ## ")
 		written := false
-		if data.SafeOutputs.AddIssueComment != nil {
+		if data.SafeOutputs.AddIssueComments != nil {
 			yaml.WriteString("Adding a Comment to an Issue or Pull Request")
 			written = true
 		}
-		if data.SafeOutputs.CreateIssue != nil {
+		if data.SafeOutputs.CreateIssues != nil {
 			if written {
 				yaml.WriteString(", ")
 			}
 			yaml.WriteString("Creating an Issue")
 		}
-		if data.SafeOutputs.CreatePullRequest != nil {
+		if data.SafeOutputs.CreatePullRequests != nil {
 			if written {
 				yaml.WriteString(", ")
 			}
@@ -2297,7 +2304,7 @@ func (c *Compiler) generatePrompt(yaml *strings.Builder, data *WorkflowData, eng
 		yaml.WriteString("          ### Available Output Types:\n")
 		yaml.WriteString("          \n")
 
-		if data.SafeOutputs.AddIssueComment != nil {
+		if data.SafeOutputs.AddIssueComments != nil {
 			yaml.WriteString("          **Adding a Comment to an Issue or Pull Request**\n")
 			yaml.WriteString("          ```json\n")
 			yaml.WriteString("          {\"type\": \"add-issue-comment\", \"body\": \"Your comment content in markdown\"}\n")
@@ -2305,7 +2312,7 @@ func (c *Compiler) generatePrompt(yaml *strings.Builder, data *WorkflowData, eng
 			yaml.WriteString("          \n")
 		}
 
-		if data.SafeOutputs.CreateIssue != nil {
+		if data.SafeOutputs.CreateIssues != nil {
 			yaml.WriteString("          **Creating an Issue**\n")
 			yaml.WriteString("          ```json\n")
 			yaml.WriteString("          {\"type\": \"create-issue\", \"title\": \"Issue title\", \"body\": \"Issue body in markdown\", \"labels\": [\"optional\", \"labels\"]}\n")
@@ -2313,7 +2320,7 @@ func (c *Compiler) generatePrompt(yaml *strings.Builder, data *WorkflowData, eng
 			yaml.WriteString("          \n")
 		}
 
-		if data.SafeOutputs.CreatePullRequest != nil {
+		if data.SafeOutputs.CreatePullRequests != nil {
 			yaml.WriteString("          **Creating a Pull Request**\n")
 			yaml.WriteString("          \n")
 			yaml.WriteString("          To create a pull request:\n")
@@ -2339,15 +2346,15 @@ func (c *Compiler) generatePrompt(yaml *strings.Builder, data *WorkflowData, eng
 
 		// Generate conditional examples based on enabled SafeOutputs
 		exampleCount := 0
-		if data.SafeOutputs.CreateIssue != nil {
+		if data.SafeOutputs.CreateIssues != nil {
 			yaml.WriteString("          {\"type\": \"create-issue\", \"title\": \"Bug Report\", \"body\": \"Found an issue with...\"}\n")
 			exampleCount++
 		}
-		if data.SafeOutputs.AddIssueComment != nil {
+		if data.SafeOutputs.AddIssueComments != nil {
 			yaml.WriteString("          {\"type\": \"add-issue-comment\", \"body\": \"This is related to the issue above.\"}\n")
 			exampleCount++
 		}
-		if data.SafeOutputs.CreatePullRequest != nil {
+		if data.SafeOutputs.CreatePullRequests != nil {
 			yaml.WriteString("          {\"type\": \"create-pull-request\", \"title\": \"Fix typo\", \"body\": \"Corrected spelling mistake in documentation\"}\n")
 			exampleCount++
 		}
@@ -2417,86 +2424,22 @@ func (c *Compiler) extractSafeOutputsConfig(frontmatter map[string]any) *SafeOut
 		if outputMap, ok := output.(map[string]any); ok {
 			config := &SafeOutputsConfig{}
 
-			// Parse create-issue configuration
-			if issue, exists := outputMap["create-issue"]; exists {
-				if issueMap, ok := issue.(map[string]any); ok {
-					issueConfig := &CreateIssueConfig{}
-
-					// Parse title-prefix
-					if titlePrefix, exists := issueMap["title-prefix"]; exists {
-						if titlePrefixStr, ok := titlePrefix.(string); ok {
-							issueConfig.TitlePrefix = titlePrefixStr
-						}
-					}
-
-					// Parse labels
-					if labels, exists := issueMap["labels"]; exists {
-						if labelsArray, ok := labels.([]any); ok {
-							var labelStrings []string
-							for _, label := range labelsArray {
-								if labelStr, ok := label.(string); ok {
-									labelStrings = append(labelStrings, labelStr)
-								}
-							}
-							issueConfig.Labels = labelStrings
-						}
-					}
-
-					config.CreateIssue = issueConfig
-				} else if issue == nil {
-					// Handle null case: create empty config
-					config.CreateIssue = &CreateIssueConfig{}
-				}
+			// Handle create-issue and create-issues
+			issuesConfig := c.parseIssuesConfig(outputMap)
+			if issuesConfig != nil {
+				config.CreateIssues = issuesConfig
 			}
 
-			// Parse add-issue-comment configuration
-			if comment, exists := outputMap["add-issue-comment"]; exists {
-				if _, ok := comment.(map[string]any); ok {
-					// For now, CommentConfig is an empty struct
-					config.AddIssueComment = &AddIssueCommentConfig{}
-				} else if comment == nil {
-					// Handle null case: create empty config
-					config.AddIssueComment = &AddIssueCommentConfig{}
-				}
+			// Handle add-issue-comment and add-issue-comments
+			commentsConfig := c.parseCommentsConfig(outputMap)
+			if commentsConfig != nil {
+				config.AddIssueComments = commentsConfig
 			}
 
-			// Parse create-pull-request configuration
-			if pullRequest, exists := outputMap["create-pull-request"]; exists {
-				if pullRequestMap, ok := pullRequest.(map[string]any); ok {
-					pullRequestConfig := &CreatePullRequestConfig{}
-
-					// Parse title-prefix
-					if titlePrefix, exists := pullRequestMap["title-prefix"]; exists {
-						if titlePrefixStr, ok := titlePrefix.(string); ok {
-							pullRequestConfig.TitlePrefix = titlePrefixStr
-						}
-					}
-
-					// Parse labels
-					if labels, exists := pullRequestMap["labels"]; exists {
-						if labelsArray, ok := labels.([]any); ok {
-							var labelStrings []string
-							for _, label := range labelsArray {
-								if labelStr, ok := label.(string); ok {
-									labelStrings = append(labelStrings, labelStr)
-								}
-							}
-							pullRequestConfig.Labels = labelStrings
-						}
-					}
-
-					// Parse draft
-					if draft, exists := pullRequestMap["draft"]; exists {
-						if draftBool, ok := draft.(bool); ok {
-							pullRequestConfig.Draft = &draftBool
-						}
-					}
-
-					config.CreatePullRequest = pullRequestConfig
-				} else if pullRequest == nil {
-					// Handle null case: create empty config
-					config.CreatePullRequest = &CreatePullRequestConfig{}
-				}
+			// Handle create-pull-request and create-pull-requests
+			pullRequestsConfig := c.parsePullRequestsConfig(outputMap)
+			if pullRequestsConfig != nil {
+				config.CreatePullRequests = pullRequestsConfig
 			}
 
 			// Parse allowed-domains configuration
@@ -2567,6 +2510,196 @@ func (c *Compiler) extractSafeOutputsConfig(frontmatter map[string]any) *SafeOut
 	return nil
 }
 
+// parseIssuesConfig handles both create-issue (singular) and create-issues (plural) configurations
+func (c *Compiler) parseIssuesConfig(outputMap map[string]any) *CreateIssuesConfig {
+	// Check for both singular and plural forms
+	hasSingular := false
+	hasPlural := false
+
+	if _, exists := outputMap["create-issue"]; exists {
+		hasSingular = true
+	}
+	if _, exists := outputMap["create-issues"]; exists {
+		hasPlural = true
+	}
+
+	// Error if both are specified
+	if hasSingular && hasPlural {
+		// This should be caught by validation, but we'll handle it gracefully
+		// Prefer plural form
+		hasSingular = false
+	}
+
+	var configData any
+	var defaultMax int
+
+	if hasPlural {
+		configData = outputMap["create-issues"]
+		defaultMax = 10 // Default for plural form
+	} else if hasSingular {
+		configData = outputMap["create-issue"]
+		defaultMax = 1 // Singular form always has max 1
+	} else {
+		return nil
+	}
+
+	issuesConfig := &CreateIssuesConfig{Max: defaultMax}
+
+	if configMap, ok := configData.(map[string]any); ok {
+		// Parse title-prefix
+		if titlePrefix, exists := configMap["title-prefix"]; exists {
+			if titlePrefixStr, ok := titlePrefix.(string); ok {
+				issuesConfig.TitlePrefix = titlePrefixStr
+			}
+		}
+
+		// Parse labels
+		if labels, exists := configMap["labels"]; exists {
+			if labelsArray, ok := labels.([]any); ok {
+				var labelStrings []string
+				for _, label := range labelsArray {
+					if labelStr, ok := label.(string); ok {
+						labelStrings = append(labelStrings, labelStr)
+					}
+				}
+				issuesConfig.Labels = labelStrings
+			}
+		}
+
+		// Parse max (only for plural form)
+		if hasPlural {
+			if max, exists := configMap["max"]; exists {
+				if maxInt, ok := c.parseIntValue(max); ok {
+					issuesConfig.Max = maxInt
+				}
+			}
+		}
+	}
+
+	return issuesConfig
+}
+
+// parseCommentsConfig handles both add-issue-comment (singular) and add-issue-comments (plural) configurations
+func (c *Compiler) parseCommentsConfig(outputMap map[string]any) *AddIssueCommentsConfig {
+	// Check for both singular and plural forms
+	hasSingular := false
+	hasPlural := false
+
+	if _, exists := outputMap["add-issue-comment"]; exists {
+		hasSingular = true
+	}
+	if _, exists := outputMap["add-issue-comments"]; exists {
+		hasPlural = true
+	}
+
+	// Error if both are specified
+	if hasSingular && hasPlural {
+		// This should be caught by validation, but we'll handle it gracefully
+		// Prefer plural form
+		hasSingular = false
+	}
+
+	var configData any
+	var defaultMax int
+
+	if hasPlural {
+		configData = outputMap["add-issue-comments"]
+		defaultMax = 10 // Default for plural form
+	} else if hasSingular {
+		configData = outputMap["add-issue-comment"]
+		defaultMax = 1 // Singular form always has max 1
+	} else {
+		return nil
+	}
+
+	commentsConfig := &AddIssueCommentsConfig{Max: defaultMax}
+
+	if configMap, ok := configData.(map[string]any); ok {
+		// Parse max (only for plural form)
+		if hasPlural {
+			if max, exists := configMap["max"]; exists {
+				if maxInt, ok := c.parseIntValue(max); ok {
+					commentsConfig.Max = maxInt
+				}
+			}
+		}
+	}
+
+	return commentsConfig
+}
+
+// parsePullRequestsConfig handles only create-pull-request (singular) configuration
+func (c *Compiler) parsePullRequestsConfig(outputMap map[string]any) *CreatePullRequestsConfig {
+	// Check for singular form only
+	hasSingular := false
+	if _, exists := outputMap["create-pull-request"]; exists {
+		hasSingular = true
+	}
+
+	// Check for unsupported plural form and return nil (no error, just ignore)
+	if _, exists := outputMap["create-pull-requests"]; exists {
+		// Plural form is not supported for pull requests - ignore it
+		return nil
+	}
+
+	if !hasSingular {
+		return nil
+	}
+
+	configData := outputMap["create-pull-request"]
+	pullRequestsConfig := &CreatePullRequestsConfig{Max: 1} // Always max 1 for pull requests
+
+	if configMap, ok := configData.(map[string]any); ok {
+		// Parse title-prefix
+		if titlePrefix, exists := configMap["title-prefix"]; exists {
+			if titlePrefixStr, ok := titlePrefix.(string); ok {
+				pullRequestsConfig.TitlePrefix = titlePrefixStr
+			}
+		}
+
+		// Parse labels
+		if labels, exists := configMap["labels"]; exists {
+			if labelsArray, ok := labels.([]any); ok {
+				var labelStrings []string
+				for _, label := range labelsArray {
+					if labelStr, ok := label.(string); ok {
+						labelStrings = append(labelStrings, labelStr)
+					}
+				}
+				pullRequestsConfig.Labels = labelStrings
+			}
+		}
+
+		// Parse draft
+		if draft, exists := configMap["draft"]; exists {
+			if draftBool, ok := draft.(bool); ok {
+				pullRequestsConfig.Draft = &draftBool
+			}
+		}
+
+		// Note: max parameter is not supported for pull requests (always limited to 1)
+		// If max is specified, it will be ignored as pull requests are singular only
+	}
+
+	return pullRequestsConfig
+}
+
+// parseIntValue safely parses various numeric types to int
+func (c *Compiler) parseIntValue(value any) (int, bool) {
+	switch v := value.(type) {
+	case int:
+		return v, true
+	case int64:
+		return int(v), true
+	case uint64:
+		return int(v), true
+	case float64:
+		return int(v), true
+	default:
+		return 0, false
+	}
+}
+
 // buildCustomJobs creates custom jobs defined in the frontmatter jobs section
 func (c *Compiler) buildCustomJobs(data *WorkflowData) error {
 	for jobName, jobConfig := range data.Jobs {
@@ -2844,13 +2977,13 @@ func (c *Compiler) generateOutputCollectionStep(yaml *strings.Builder, data *Wor
 	if data.SafeOutputs != nil {
 		// Create a simplified config object for validation
 		safeOutputsConfig := make(map[string]interface{})
-		if data.SafeOutputs.CreateIssue != nil {
+		if data.SafeOutputs.CreateIssues != nil {
 			safeOutputsConfig["create-issue"] = true
 		}
-		if data.SafeOutputs.AddIssueComment != nil {
+		if data.SafeOutputs.AddIssueComments != nil {
 			safeOutputsConfig["add-issue-comment"] = true
 		}
-		if data.SafeOutputs.CreatePullRequest != nil {
+		if data.SafeOutputs.CreatePullRequests != nil {
 			safeOutputsConfig["create-pull-request"] = true
 		}
 		if data.SafeOutputs.AddIssueLabels != nil {
diff --git a/pkg/workflow/compiler_test.go b/pkg/workflow/compiler_test.go
index 6f823226858..c4adaf799c8 100644
--- a/pkg/workflow/compiler_test.go
+++ b/pkg/workflow/compiler_test.go
@@ -5428,7 +5428,7 @@ func TestComputeAllowedToolsWithSafeOutputs(t *testing.T) {
 				},
 			},
 			safeOutputs: &SafeOutputsConfig{
-				CreateIssue: &CreateIssueConfig{},
+				CreateIssues: &CreateIssuesConfig{Max: 1},
 			},
 			expected: "Read,Write",
 		},
@@ -5443,7 +5443,7 @@ func TestComputeAllowedToolsWithSafeOutputs(t *testing.T) {
 				},
 			},
 			safeOutputs: &SafeOutputsConfig{
-				CreateIssue: &CreateIssueConfig{},
+				CreateIssues: &CreateIssuesConfig{Max: 1},
 			},
 			expected: "Read,Write",
 		},
@@ -5469,9 +5469,9 @@ func TestComputeAllowedToolsWithSafeOutputs(t *testing.T) {
 				},
 			},
 			safeOutputs: &SafeOutputsConfig{
-				CreateIssue:       &CreateIssueConfig{},
-				AddIssueComment:   &AddIssueCommentConfig{},
-				CreatePullRequest: &CreatePullRequestConfig{},
+				CreateIssues:       &CreateIssuesConfig{Max: 1},
+				AddIssueComments:   &AddIssueCommentsConfig{Max: 1},
+				CreatePullRequests: &CreatePullRequestsConfig{Max: 1},
 			},
 			expected: "Bash,Write",
 		},
@@ -5488,7 +5488,7 @@ func TestComputeAllowedToolsWithSafeOutputs(t *testing.T) {
 				},
 			},
 			safeOutputs: &SafeOutputsConfig{
-				CreateIssue: &CreateIssueConfig{},
+				CreateIssues: &CreateIssuesConfig{Max: 1},
 			},
 			expected: "Read,Write,mcp__github__create_issue,mcp__github__create_pull_request",
 		},
diff --git a/pkg/workflow/js/collect_ndjson_output.cjs b/pkg/workflow/js/collect_ndjson_output.cjs
index 894f5da9f09..3ad997fa9f1 100644
--- a/pkg/workflow/js/collect_ndjson_output.cjs
+++ b/pkg/workflow/js/collect_ndjson_output.cjs
@@ -1,130 +1,157 @@
-/**
- * Sanitizes content for safe output in GitHub Actions
- * @param {string} content - The content to sanitize
- * @returns {string} The sanitized content
- */
-function sanitizeContent(content) {
-  if (!content || typeof content !== 'string') {
-    return '';
-  }
+async function main() {
+  const fs = require("fs");
+  
+  /**
+   * Sanitizes content for safe output in GitHub Actions
+   * @param {string} content - The content to sanitize
+   * @returns {string} The sanitized content
+   */
+  function sanitizeContent(content) {
+    if (!content || typeof content !== 'string') {
+      return '';
+    }
 
-  // Read allowed domains from environment variable
-  const allowedDomainsEnv = process.env.GITHUB_AW_ALLOWED_DOMAINS;
-  const defaultAllowedDomains = [
-    'github.com',
-    'github.io',
-    'githubusercontent.com',
-    'githubassets.com',
-    'github.dev',
-    'codespaces.new'
-  ];
-
-  const allowedDomains = allowedDomainsEnv
-    ? allowedDomainsEnv.split(',').map(d => d.trim()).filter(d => d)
-    : defaultAllowedDomains;
-
-  let sanitized = content;
-
-  // Neutralize @mentions to prevent unintended notifications
-  sanitized = neutralizeMentions(sanitized);
-
-  // Remove control characters (except newlines and tabs)
-  sanitized = sanitized.replace(/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/g, '');
-
-  // XML character escaping
-  sanitized = sanitized
-    .replace(/&/g, '&amp;')   // Must be first to avoid double-escaping
-    .replace(/</g, '&lt;')
-    .replace(/>/g, '&gt;')
-    .replace(/"/g, '&quot;')
-    .replace(/'/g, '&apos;');
-
-  // URI filtering - replace non-https protocols with "(redacted)"
-  sanitized = sanitizeUrlProtocols(sanitized);
-
-  // Domain filtering for HTTPS URIs
-  sanitized = sanitizeUrlDomains(sanitized);
-
-  // Limit total length to prevent DoS (0.5MB max)
-  const maxLength = 524288;
-  if (sanitized.length > maxLength) {
-    sanitized = sanitized.substring(0, maxLength) + '\n[Content truncated due to length]';
-  }
+    // Read allowed domains from environment variable
+    const allowedDomainsEnv = process.env.GITHUB_AW_ALLOWED_DOMAINS;
+    const defaultAllowedDomains = [
+      'github.com',
+      'github.io',
+      'githubusercontent.com',
+      'githubassets.com',
+      'github.dev',
+      'codespaces.new'
+    ];
+
+    const allowedDomains = allowedDomainsEnv
+      ? allowedDomainsEnv.split(',').map(d => d.trim()).filter(d => d)
+      : defaultAllowedDomains;
+
+    let sanitized = content;
+
+    // Neutralize @mentions to prevent unintended notifications
+    sanitized = neutralizeMentions(sanitized);
+
+    // Remove control characters (except newlines and tabs)
+    sanitized = sanitized.replace(/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/g, '');
+
+    // XML character escaping
+    sanitized = sanitized
+      .replace(/&/g, '&amp;')   // Must be first to avoid double-escaping
+      .replace(/</g, '&lt;')
+      .replace(/>/g, '&gt;')
+      .replace(/"/g, '&quot;')
+      .replace(/'/g, '&apos;');
+
+    // URI filtering - replace non-https protocols with "(redacted)"
+    sanitized = sanitizeUrlProtocols(sanitized);
+
+    // Domain filtering for HTTPS URIs
+    sanitized = sanitizeUrlDomains(sanitized);
+
+    // Limit total length to prevent DoS (0.5MB max)
+    const maxLength = 524288;
+    if (sanitized.length > maxLength) {
+      sanitized = sanitized.substring(0, maxLength) + '\n[Content truncated due to length]';
+    }
 
-  // Limit number of lines to prevent log flooding (65k max)
-  const lines = sanitized.split('\n');
-  const maxLines = 65000;
-  if (lines.length > maxLines) {
-    sanitized = lines.slice(0, maxLines).join('\n') + '\n[Content truncated due to line count]';
-  }
+    // Limit number of lines to prevent log flooding (65k max)
+    const lines = sanitized.split('\n');
+    const maxLines = 65000;
+    if (lines.length > maxLines) {
+      sanitized = lines.slice(0, maxLines).join('\n') + '\n[Content truncated due to line count]';
+    }
 
-  // Remove ANSI escape sequences
-  sanitized = sanitized.replace(/\x1b\[[0-9;]*[mGKH]/g, '');
+    // Remove ANSI escape sequences
+    sanitized = sanitized.replace(/\x1b\[[0-9;]*[mGKH]/g, '');
 
-  // Neutralize common bot trigger phrases
-  sanitized = neutralizeBotTriggers(sanitized);
+    // Neutralize common bot trigger phrases
+    sanitized = neutralizeBotTriggers(sanitized);
 
-  // Trim excessive whitespace
-  return sanitized.trim();
+    // Trim excessive whitespace
+    return sanitized.trim();
 
-  /**
-   * Remove unknown domains
-   * @param {string} s - The string to process
-   * @returns {string} The string with unknown domains redacted
-   */
-  function sanitizeUrlDomains(s) {
-    return s.replace(/\bhttps:\/\/([^\/\s\])}'"<>&\x00-\x1f]+)/gi, (match, domain) => {
-      // Extract the hostname part (before first slash, colon, or other delimiter)
-      const hostname = domain.split(/[\/:\?#]/)[0].toLowerCase();
-
-      // Check if this domain or any parent domain is in the allowlist
-      const isAllowed = allowedDomains.some(allowedDomain => {
-        const normalizedAllowed = allowedDomain.toLowerCase();
-        return hostname === normalizedAllowed || hostname.endsWith('.' + normalizedAllowed);
+    /**
+     * Remove unknown domains
+     * @param {string} s - The string to process
+     * @returns {string} The string with unknown domains redacted
+     */
+    function sanitizeUrlDomains(s) {
+      return s.replace(/\bhttps:\/\/([^\/\s\])}'"<>&\x00-\x1f]+)/gi, (match, domain) => {
+        // Extract the hostname part (before first slash, colon, or other delimiter)
+        const hostname = domain.split(/[\/:\?#]/)[0].toLowerCase();
+
+        // Check if this domain or any parent domain is in the allowlist
+        const isAllowed = allowedDomains.some(allowedDomain => {
+          const normalizedAllowed = allowedDomain.toLowerCase();
+          return hostname === normalizedAllowed || hostname.endsWith('.' + normalizedAllowed);
+        });
+
+        return isAllowed ? match : '(redacted)';
       });
+    }
 
-      return isAllowed ? match : '(redacted)';
-    });
-  }
+    /**
+     * Remove unknown protocols except https
+     * @param {string} s - The string to process
+     * @returns {string} The string with non-https protocols redacted
+     */
+    function sanitizeUrlProtocols(s) {
+      // Match both protocol:// and protocol: patterns
+      return s.replace(/\b(\w+):(?:\/\/)?[^\s\])}'"<>&\x00-\x1f]+/gi, (match, protocol) => {
+        // Allow https (case insensitive), redact everything else
+        return protocol.toLowerCase() === 'https' ? match : '(redacted)';
+      });
+    }
 
-  /**
-   * Remove unknown protocols except https
-   * @param {string} s - The string to process
-   * @returns {string} The string with non-https protocols redacted
-   */
-  function sanitizeUrlProtocols(s) {
-    // Match both protocol:// and protocol: patterns
-    return s.replace(/\b(\w+):(?:\/\/)?[^\s\])}'"<>&\x00-\x1f]+/gi, (match, protocol) => {
-      // Allow https (case insensitive), redact everything else
-      return protocol.toLowerCase() === 'https' ? match : '(redacted)';
-    });
-  }
+    /**
+     * Neutralizes @mentions by wrapping them in backticks
+     * @param {string} s - The string to process
+     * @returns {string} The string with neutralized mentions
+     */
+    function neutralizeMentions(s) {
+      // Replace @name or @org/team outside code with `@name`
+      return s.replace(/(^|[^\w`])@([A-Za-z0-9](?:[A-Za-z0-9-]{0,37}[A-Za-z0-9])?(?:\/[A-Za-z0-9._-]+)?)/g,
+        (_m, p1, p2) => `${p1}\`@${p2}\``);
+    }
 
-  /**
-   * Neutralizes @mentions by wrapping them in backticks
-   * @param {string} s - The string to process
-   * @returns {string} The string with neutralized mentions
-   */
-  function neutralizeMentions(s) {
-    // Replace @name or @org/team outside code with `@name`
-    return s.replace(/(^|[^\w`])@([A-Za-z0-9](?:[A-Za-z0-9-]{0,37}[A-Za-z0-9])?(?:\/[A-Za-z0-9._-]+)?)/g,
-      (_m, p1, p2) => `${p1}\`@${p2}\``);
+    /**
+     * Neutralizes bot trigger phrases by wrapping them in backticks
+     * @param {string} s - The string to process
+     * @returns {string} The string with neutralized bot triggers
+     */
+    function neutralizeBotTriggers(s) {
+      // Neutralize common bot trigger phrases like "fixes #123", "closes #asdfs", etc.
+      return s.replace(/\b(fixes?|closes?|resolves?|fix|close|resolve)\s+#(\w+)/gi,
+        (match, action, ref) => `\`${action} #${ref}\``);
+    }
   }
-
+  
   /**
-   * Neutralizes bot trigger phrases by wrapping them in backticks
-   * @param {string} s - The string to process
-   * @returns {string} The string with neutralized bot triggers
+   * Gets the maximum allowed count for a given output type
+   * @param {string} itemType - The output item type
+   * @param {Object} config - The safe-outputs configuration
+   * @returns {number} The maximum allowed count
    */
-  function neutralizeBotTriggers(s) {
-    // Neutralize common bot trigger phrases like "fixes #123", "closes #asdfs", etc.
-    return s.replace(/\b(fixes?|closes?|resolves?|fix|close|resolve)\s+#(\w+)/gi,
-      (match, action, ref) => `\`${action} #${ref}\``);
+  function getMaxAllowedForType(itemType, config) {
+    // Check if max is explicitly specified in config
+    if (config && config[itemType] && typeof config[itemType] === 'object' && config[itemType].max) {
+      return config[itemType].max;
+    }
+    
+    // Use default limits for plural-supported types
+    switch (itemType) {
+      case 'create-issue':
+        return 10; // Allow multiple issues
+      case 'add-issue-comment':
+        return 10; // Allow multiple comments
+      case 'create-pull-request':
+        return 1;  // Only one pull request allowed
+      case 'add-issue-labels':
+        return 1;  // Only one labels operation allowed
+      default:
+        return 1;  // Default to single item for unknown types
+    }
   }
-}
-
-async function main() {
-  const fs = require("fs");
   const outputFile = process.env.GITHUB_AW_SAFE_OUTPUTS;
   const safeOutputsConfig = process.env.GITHUB_AW_SAFE_OUTPUTS_CONFIG;
   
@@ -185,10 +212,11 @@ async function main() {
         continue;
       }
 
-      // Check for duplicates based on type (since current format allows only one of each)
-      const existingItem = parsedItems.find(existing => existing.type === itemType);
-      if (existingItem) {
-        errors.push(`Line ${i + 1}: Duplicate output type '${itemType}'. Only one item of each type is currently allowed.`);
+      // Check for too many items of the same type
+      const typeCount = parsedItems.filter(existing => existing.type === itemType).length;
+      const maxAllowed = getMaxAllowedForType(itemType, expectedOutputTypes);
+      if (typeCount >= maxAllowed) {
+        errors.push(`Line ${i + 1}: Too many items of type '${itemType}'. Maximum allowed: ${maxAllowed}.`);
         continue;
       }
 
diff --git a/pkg/workflow/js/collect_ndjson_output.test.cjs b/pkg/workflow/js/collect_ndjson_output.test.cjs
index da5f4654275..17f320f4b4a 100644
--- a/pkg/workflow/js/collect_ndjson_output.test.cjs
+++ b/pkg/workflow/js/collect_ndjson_output.test.cjs
@@ -194,7 +194,7 @@ describe('collect_ndjson_output.cjs', () => {
     expect(parsedOutput.errors[0]).toContain('Invalid JSON');
   });
 
-  it('should reject duplicate output types', async () => {
+  it('should allow multiple items of supported types up to limits', async () => {
     const testFile = '/tmp/test-ndjson-output.txt';
     const ndjsonContent = `{"type": "create-issue", "title": "First Issue", "body": "First body"}
 {"type": "create-issue", "title": "Second Issue", "body": "Second body"}`;
@@ -210,10 +210,35 @@ describe('collect_ndjson_output.cjs', () => {
     expect(outputCall).toBeDefined();
     
     const parsedOutput = JSON.parse(outputCall[1]);
-    expect(parsedOutput.items).toHaveLength(1);
+    expect(parsedOutput.items).toHaveLength(2); // Both items should be allowed
     expect(parsedOutput.items[0].title).toBe('First Issue');
-    expect(parsedOutput.errors).toHaveLength(1);
-    expect(parsedOutput.errors[0]).toContain('Duplicate output type');
+    expect(parsedOutput.items[1].title).toBe('Second Issue');
+    expect(parsedOutput.errors).toHaveLength(0); // No errors for multiple items within limits
+  });
+
+  it('should respect max limits from config', async () => {
+    const testFile = '/tmp/test-ndjson-output.txt';
+    const ndjsonContent = `{"type": "create-issue", "title": "First Issue", "body": "First body"}
+{"type": "create-issue", "title": "Second Issue", "body": "Second body"}
+{"type": "create-issue", "title": "Third Issue", "body": "Third body"}`;
+    
+    fs.writeFileSync(testFile, ndjsonContent);
+    process.env.GITHUB_AW_SAFE_OUTPUTS = testFile;
+    // Set max to 2 for create-issue
+    process.env.GITHUB_AW_SAFE_OUTPUTS_CONFIG = '{"create-issue": {"max": 2}}';
+    
+    await eval(`(async () => { ${collectScript} })()`);
+    
+    const setOutputCalls = mockCore.setOutput.mock.calls;
+    const outputCall = setOutputCalls.find(call => call[0] === 'output');
+    expect(outputCall).toBeDefined();
+    
+    const parsedOutput = JSON.parse(outputCall[1]);
+    expect(parsedOutput.items).toHaveLength(2); // Only first 2 items should be allowed
+    expect(parsedOutput.items[0].title).toBe('First Issue');
+    expect(parsedOutput.items[1].title).toBe('Second Issue');
+    expect(parsedOutput.errors).toHaveLength(1); // Error for the third item exceeding max
+    expect(parsedOutput.errors[0]).toContain('Too many items of type \'create-issue\'. Maximum allowed: 2');
   });
 
   it('should skip empty lines', async () => {
diff --git a/pkg/workflow/js/create_comment.cjs b/pkg/workflow/js/create_comment.cjs
index 6dec2454985..979de66118f 100644
--- a/pkg/workflow/js/create_comment.cjs
+++ b/pkg/workflow/js/create_comment.cjs
@@ -27,14 +27,14 @@ async function main() {
     return;
   }
 
-  // Find the add-issue-comment item
-  const commentItem = validatedOutput.items.find(/** @param {any} item */ item => item.type === 'add-issue-comment');
-  if (!commentItem) {
-    console.log('No add-issue-comment item found in agent output');
+  // Find all add-issue-comment items
+  const commentItems = validatedOutput.items.filter(/** @param {any} item */ item => item.type === 'add-issue-comment');
+  if (commentItems.length === 0) {
+    console.log('No add-issue-comment items found in agent output');
     return;
   }
 
-  console.log('Found add-issue-comment item:', { bodyLength: commentItem.body.length });
+  console.log(`Found ${commentItems.length} add-issue-comment item(s)`);
 
   // Check if we're in an issue or pull request context
   const isIssueContext = context.eventName === 'issues' || context.eventName === 'issue_comment';
@@ -45,66 +45,86 @@ async function main() {
     return;
   }
 
-  // Determine the issue/PR number and comment endpoint
-  let issueNumber;
-  let commentEndpoint;
-
-  if (isIssueContext) {
-    if (context.payload.issue) {
-      issueNumber = context.payload.issue.number;
-      commentEndpoint = 'issues';
-    } else {
-      console.log('Issue context detected but no issue found in payload');
-      return;
+  const createdComments = [];
+
+  // Process each comment item
+  for (let i = 0; i < commentItems.length; i++) {
+    const commentItem = commentItems[i];
+    console.log(`Processing add-issue-comment item ${i + 1}/${commentItems.length}:`, { bodyLength: commentItem.body.length });
+
+    // Determine the issue/PR number and comment endpoint for this comment
+    let issueNumber;
+    let commentEndpoint;
+
+    if (isIssueContext) {
+      if (context.payload.issue) {
+        issueNumber = context.payload.issue.number;
+        commentEndpoint = 'issues';
+      } else {
+        console.log('Issue context detected but no issue found in payload');
+        continue;
+      }
+    } else if (isPRContext) {
+      if (context.payload.pull_request) {
+        issueNumber = context.payload.pull_request.number;
+        commentEndpoint = 'issues'; // PR comments use the issues API endpoint
+      } else {
+        console.log('Pull request context detected but no pull request found in payload');
+        continue;
+      }
     }
-  } else if (isPRContext) {
-    if (context.payload.pull_request) {
-      issueNumber = context.payload.pull_request.number;
-      commentEndpoint = 'issues'; // PR comments use the issues API endpoint
-    } else {
-      console.log('Pull request context detected but no pull request found in payload');
-      return;
+
+    if (!issueNumber) {
+      console.log('Could not determine issue or pull request number');
+      continue;
+    }
+
+    // Extract body from the JSON item
+    let body = commentItem.body.trim();
+    // Add AI disclaimer with run id, run htmlurl
+    const runId = context.runId;
+    const runUrl = context.payload.repository 
+      ? `${context.payload.repository.html_url}/actions/runs/${runId}`
+      : `https://github.com/actions/runs/${runId}`;  
+    body += `\n\n> Generated by Agentic Workflow Run [${runId}](${runUrl})\n`;
+
+    console.log(`Creating comment on ${commentEndpoint} #${issueNumber}`);
+    console.log('Comment content length:', body.length);
+
+    try {
+      // Create the comment using GitHub API
+      const { data: comment } = await github.rest.issues.createComment({
+        owner: context.repo.owner,
+        repo: context.repo.repo,
+        issue_number: issueNumber,
+        body: body
+      });
+
+      console.log('Created comment #' + comment.id + ': ' + comment.html_url);
+      createdComments.push(comment);
+
+      // Set output for the last created comment (for backward compatibility)
+      if (i === commentItems.length - 1) {
+        core.setOutput('comment_id', comment.id);
+        core.setOutput('comment_url', comment.html_url);
+      }
+    } catch (error) {
+      console.error(`✗ Failed to create comment:`, error instanceof Error ? error.message : String(error));
+      throw error;
     }
   }
 
-  if (!issueNumber) {
-    console.log('Could not determine issue or pull request number');
-    return;
+  // Write summary for all created comments
+  if (createdComments.length > 0) {
+    let summaryContent = '\n\n## GitHub Comments\n';
+    for (const comment of createdComments) {
+      summaryContent += `- Comment #${comment.id}: [View Comment](${comment.html_url})\n`;
+    }
+    await core.summary.addRaw(summaryContent).write();
   }
 
-  // Extract body from the JSON item
-  let body = commentItem.body.trim();
-  // Add AI disclaimer with run id, run htmlurl
-  const runId = context.runId;
-  const runUrl = context.payload.repository 
-    ? `${context.payload.repository.html_url}/actions/runs/${runId}`
-    : `https://github.com/actions/runs/${runId}`;  
-  body += `\n\n> Generated by Agentic Workflow Run [${runId}](${runUrl})\n`;
-
-  console.log(`Creating comment on ${commentEndpoint} #${issueNumber}`);
-  console.log('Comment content length:', body.length);
-
-  // Create the comment using GitHub API
-  const { data: comment } = await github.rest.issues.createComment({
-    owner: context.repo.owner,
-    repo: context.repo.repo,
-    issue_number: issueNumber,
-    body: body
-  });
-
-  console.log('Created comment #' + comment.id + ': ' + comment.html_url);
-
-  // Set output for other jobs to use
-  core.setOutput('comment_id', comment.id);
-  core.setOutput('comment_url', comment.html_url);
-
-  // write comment id, url to the github_step_summary
-  await core.summary.addRaw(`
-
-## GitHub Comment
-- Comment ID: ${comment.id}
-- Comment URL: ${comment.html_url}
-`).write();
+  console.log(`Successfully created ${createdComments.length} comment(s)`);
+  return createdComments;
 
 }
 await main();
\ No newline at end of file
diff --git a/pkg/workflow/js/create_issue.cjs b/pkg/workflow/js/create_issue.cjs
index ed465b377c8..bc64ee700ee 100644
--- a/pkg/workflow/js/create_issue.cjs
+++ b/pkg/workflow/js/create_issue.cjs
@@ -26,100 +26,120 @@ async function main() {
     return;
   }
 
-  // Find the create-issue item
-  const createIssueItem = validatedOutput.items.find(/** @param {any} item */ item => item.type === 'create-issue');
-  if (!createIssueItem) {
-    console.log('No create-issue item found in agent output');
+  // Find all create-issue items
+  const createIssueItems = validatedOutput.items.filter(/** @param {any} item */ item => item.type === 'create-issue');
+  if (createIssueItems.length === 0) {
+    console.log('No create-issue items found in agent output');
     return;
   }
 
-  console.log('Found create-issue item:', { title: createIssueItem.title, bodyLength: createIssueItem.body.length });
+  console.log(`Found ${createIssueItems.length} create-issue item(s)`);
 
   // Check if we're in an issue context (triggered by an issue event)
   const parentIssueNumber = context.payload?.issue?.number;
   
-  // Parse labels from environment variable (comma-separated string) or from the JSON item
+  // Parse labels from environment variable (comma-separated string)
   const labelsEnv = process.env.GITHUB_AW_ISSUE_LABELS;
-  let labels = labelsEnv ? labelsEnv.split(',').map(/** @param {string} label */ label => label.trim()).filter(/** @param {string} label */ label => label) : [];
+  let envLabels = labelsEnv ? labelsEnv.split(',').map(/** @param {string} label */ label => label.trim()).filter(/** @param {string} label */ label => label) : [];
   
-  // If the item has labels, use those instead (or merge them)
-  if (createIssueItem.labels && Array.isArray(createIssueItem.labels)) {
-    labels = [...labels, ...createIssueItem.labels].filter(Boolean);
-  }
-
-  // Extract title and body from the JSON item
-  let title = createIssueItem.title ? createIssueItem.title.trim() : '';
-  let bodyLines = createIssueItem.body.split('\n');
-
-  // If no title was found, use the body content as title (or a default)
-  if (!title) {
-    title = createIssueItem.body || 'Agent Output';
-  }
-
-  // Apply title prefix if provided via environment variable
-  const titlePrefix = process.env.GITHUB_AW_ISSUE_TITLE_PREFIX;
-  if (titlePrefix && !title.startsWith(titlePrefix)) {
-    title = titlePrefix + title;
-  }
+  const createdIssues = [];
+  
+  // Process each create-issue item
+  for (let i = 0; i < createIssueItems.length; i++) {
+    const createIssueItem = createIssueItems[i];
+    console.log(`Processing create-issue item ${i + 1}/${createIssueItems.length}:`, { title: createIssueItem.title, bodyLength: createIssueItem.body.length });
+
+    // Merge environment labels with item-specific labels
+    let labels = [...envLabels];
+    if (createIssueItem.labels && Array.isArray(createIssueItem.labels)) {
+      labels = [...labels, ...createIssueItem.labels].filter(Boolean);
+    }
 
-  if (parentIssueNumber) {
-    console.log('Detected issue context, parent issue #' + parentIssueNumber);
+    // Extract title and body from the JSON item
+    let title = createIssueItem.title ? createIssueItem.title.trim() : '';
+    let bodyLines = createIssueItem.body.split('\n');
 
-    // Add reference to parent issue in the child issue body
-    bodyLines.push(`Related to #${parentIssueNumber}`);
-  }
+    // If no title was found, use the body content as title (or a default)
+    if (!title) {
+      title = createIssueItem.body || 'Agent Output';
+    }
 
-  // Add AI disclaimer with run id, run htmlurl
-  // Add AI disclaimer with workflow run information
-  const runId = context.runId;
-  const runUrl = context.payload.repository 
-    ? `${context.payload.repository.html_url}/actions/runs/${runId}`
-    : `https://github.com/actions/runs/${runId}`;  
-  bodyLines.push(``, ``, `> Generated by Agentic Workflow Run [${runId}](${runUrl})`, '');
+    // Apply title prefix if provided via environment variable
+    const titlePrefix = process.env.GITHUB_AW_ISSUE_TITLE_PREFIX;
+    if (titlePrefix && !title.startsWith(titlePrefix)) {
+      title = titlePrefix + title;
+    }
 
-  // Prepare the body content
-  const body = bodyLines.join('\n').trim();
+    if (parentIssueNumber) {
+      console.log('Detected issue context, parent issue #' + parentIssueNumber);
 
-  console.log('Creating issue with title:', title);
-  console.log('Labels:', labels);
-  console.log('Body length:', body.length);
+      // Add reference to parent issue in the child issue body
+      bodyLines.push(`Related to #${parentIssueNumber}`);
+    }
 
+    // Add AI disclaimer with run id, run htmlurl
+    // Add AI disclaimer with workflow run information
+    const runId = context.runId;
+    const runUrl = context.payload.repository 
+      ? `${context.payload.repository.html_url}/actions/runs/${runId}`
+      : `https://github.com/actions/runs/${runId}`;  
+    bodyLines.push(``, ``, `> Generated by Agentic Workflow Run [${runId}](${runUrl})`, '');
 
-  // Create the issue using GitHub API
-  const { data: issue } = await github.rest.issues.create({
-    owner: context.repo.owner,
-    repo: context.repo.repo,
-    title: title,
-    body: body,
-    labels: labels
-  });
+    // Prepare the body content
+    const body = bodyLines.join('\n').trim();
 
-  console.log('Created issue #' + issue.number + ': ' + issue.html_url);
+    console.log('Creating issue with title:', title);
+    console.log('Labels:', labels);
+    console.log('Body length:', body.length);
 
-  // If we have a parent issue, add a comment to it referencing the new child issue
-  if (parentIssueNumber) {
     try {
-      await github.rest.issues.createComment({
+      // Create the issue using GitHub API
+      const { data: issue } = await github.rest.issues.create({
         owner: context.repo.owner,
         repo: context.repo.repo,
-        issue_number: parentIssueNumber,
-        body: `Created related issue: #${issue.number}`
+        title: title,
+        body: body,
+        labels: labels
       });
-      console.log('Added comment to parent issue #' + parentIssueNumber);
+
+      console.log('Created issue #' + issue.number + ': ' + issue.html_url);
+      createdIssues.push(issue);
+
+      // If we have a parent issue, add a comment to it referencing the new child issue
+      if (parentIssueNumber) {
+        try {
+          await github.rest.issues.createComment({
+            owner: context.repo.owner,
+            repo: context.repo.repo,
+            issue_number: parentIssueNumber,
+            body: `Created related issue: #${issue.number}`
+          });
+          console.log('Added comment to parent issue #' + parentIssueNumber);
+        } catch (error) {
+          console.log('Warning: Could not add comment to parent issue:', error instanceof Error ? error.message : String(error));
+        }
+      }
+
+      // Set output for the last created issue (for backward compatibility)
+      if (i === createIssueItems.length - 1) {
+        core.setOutput('issue_number', issue.number);
+        core.setOutput('issue_url', issue.html_url);
+      }
     } catch (error) {
-      console.log('Warning: Could not add comment to parent issue:', error instanceof Error ? error.message : String(error));
+      console.error(`✗ Failed to create issue "${title}":`, error instanceof Error ? error.message : String(error));
+      throw error;
     }
   }
 
-  // Set output for other jobs to use
-  core.setOutput('issue_number', issue.number);
-  core.setOutput('issue_url', issue.html_url);
-  // write issue to summary
-  await core.summary.addRaw(`
+  // Write summary for all created issues
+  if (createdIssues.length > 0) {
+    let summaryContent = '\n\n## GitHub Issues\n';
+    for (const issue of createdIssues) {
+      summaryContent += `- Issue #${issue.number}: [${issue.title}](${issue.html_url})\n`;
+    }
+    await core.summary.addRaw(summaryContent).write();
+  }
 
-## GitHub Issue
-- Issue ID: ${issue.number}
-- Issue URL: ${issue.html_url}
-`).write();
+  console.log(`Successfully created ${createdIssues.length} issue(s)`);
 }
 await main();
\ No newline at end of file
diff --git a/pkg/workflow/output_test.go b/pkg/workflow/output_test.go
index b19014ab0cc..65fc0d26114 100644
--- a/pkg/workflow/output_test.go
+++ b/pkg/workflow/output_test.go
@@ -51,25 +51,25 @@ This workflow tests the output configuration parsing.
 		t.Fatal("Expected output configuration to be parsed")
 	}
 
-	if workflowData.SafeOutputs.CreateIssue == nil {
+	if workflowData.SafeOutputs.CreateIssues == nil {
 		t.Fatal("Expected issue configuration to be parsed")
 	}
 
 	// Verify title prefix
 	expectedPrefix := "[genai] "
-	if workflowData.SafeOutputs.CreateIssue.TitlePrefix != expectedPrefix {
-		t.Errorf("Expected title prefix '%s', got '%s'", expectedPrefix, workflowData.SafeOutputs.CreateIssue.TitlePrefix)
+	if workflowData.SafeOutputs.CreateIssues.TitlePrefix != expectedPrefix {
+		t.Errorf("Expected title prefix '%s', got '%s'", expectedPrefix, workflowData.SafeOutputs.CreateIssues.TitlePrefix)
 	}
 
 	// Verify labels
 	expectedLabels := []string{"copilot", "automation"}
-	if len(workflowData.SafeOutputs.CreateIssue.Labels) != len(expectedLabels) {
-		t.Errorf("Expected %d labels, got %d", len(expectedLabels), len(workflowData.SafeOutputs.CreateIssue.Labels))
+	if len(workflowData.SafeOutputs.CreateIssues.Labels) != len(expectedLabels) {
+		t.Errorf("Expected %d labels, got %d", len(expectedLabels), len(workflowData.SafeOutputs.CreateIssues.Labels))
 	}
 
 	for i, expectedLabel := range expectedLabels {
-		if i >= len(workflowData.SafeOutputs.CreateIssue.Labels) || workflowData.SafeOutputs.CreateIssue.Labels[i] != expectedLabel {
-			t.Errorf("Expected label '%s' at index %d, got '%s'", expectedLabel, i, workflowData.SafeOutputs.CreateIssue.Labels[i])
+		if i >= len(workflowData.SafeOutputs.CreateIssues.Labels) || workflowData.SafeOutputs.CreateIssues.Labels[i] != expectedLabel {
+			t.Errorf("Expected label '%s' at index %d, got '%s'", expectedLabel, i, workflowData.SafeOutputs.CreateIssues.Labels[i])
 		}
 	}
 }
@@ -162,29 +162,29 @@ This workflow tests the null output configuration parsing.
 	}
 
 	// Verify create-issue configuration is parsed with empty values
-	if workflowData.SafeOutputs.CreateIssue == nil {
+	if workflowData.SafeOutputs.CreateIssues == nil {
 		t.Fatal("Expected create-issue configuration to be parsed with null value")
 	}
-	if workflowData.SafeOutputs.CreateIssue.TitlePrefix != "" {
-		t.Errorf("Expected empty title prefix for null create-issue, got '%s'", workflowData.SafeOutputs.CreateIssue.TitlePrefix)
+	if workflowData.SafeOutputs.CreateIssues.TitlePrefix != "" {
+		t.Errorf("Expected empty title prefix for null create-issue, got '%s'", workflowData.SafeOutputs.CreateIssues.TitlePrefix)
 	}
-	if len(workflowData.SafeOutputs.CreateIssue.Labels) != 0 {
-		t.Errorf("Expected empty labels for null create-issue, got %v", workflowData.SafeOutputs.CreateIssue.Labels)
+	if len(workflowData.SafeOutputs.CreateIssues.Labels) != 0 {
+		t.Errorf("Expected empty labels for null create-issue, got %v", workflowData.SafeOutputs.CreateIssues.Labels)
 	}
 
 	// Verify create-pull-request configuration is parsed with empty values
-	if workflowData.SafeOutputs.CreatePullRequest == nil {
+	if workflowData.SafeOutputs.CreatePullRequests == nil {
 		t.Fatal("Expected create-pull-request configuration to be parsed with null value")
 	}
-	if workflowData.SafeOutputs.CreatePullRequest.TitlePrefix != "" {
-		t.Errorf("Expected empty title prefix for null create-pull-request, got '%s'", workflowData.SafeOutputs.CreatePullRequest.TitlePrefix)
+	if workflowData.SafeOutputs.CreatePullRequests.TitlePrefix != "" {
+		t.Errorf("Expected empty title prefix for null create-pull-request, got '%s'", workflowData.SafeOutputs.CreatePullRequests.TitlePrefix)
 	}
-	if len(workflowData.SafeOutputs.CreatePullRequest.Labels) != 0 {
-		t.Errorf("Expected empty labels for null create-pull-request, got %v", workflowData.SafeOutputs.CreatePullRequest.Labels)
+	if len(workflowData.SafeOutputs.CreatePullRequests.Labels) != 0 {
+		t.Errorf("Expected empty labels for null create-pull-request, got %v", workflowData.SafeOutputs.CreatePullRequests.Labels)
 	}
 
 	// Verify add-issue-comment configuration is parsed with empty values
-	if workflowData.SafeOutputs.AddIssueComment == nil {
+	if workflowData.SafeOutputs.AddIssueComments == nil {
 		t.Fatal("Expected add-issue-comment configuration to be parsed with null value")
 	}
 
@@ -332,7 +332,7 @@ This workflow tests the output.add-issue-comment configuration parsing.
 		t.Fatal("Expected output configuration to be parsed")
 	}
 
-	if workflowData.SafeOutputs.AddIssueComment == nil {
+	if workflowData.SafeOutputs.AddIssueComments == nil {
 		t.Fatal("Expected issue_comment configuration to be parsed")
 	}
 }
@@ -382,7 +382,7 @@ This workflow tests the output.add-issue-comment configuration parsing with null
 		t.Fatal("Expected output configuration to be parsed")
 	}
 
-	if workflowData.SafeOutputs.AddIssueComment == nil {
+	if workflowData.SafeOutputs.AddIssueComments == nil {
 		t.Fatal("Expected issue_comment configuration to be parsed even with null value")
 	}
 }
@@ -580,25 +580,25 @@ This workflow tests the output pull request configuration parsing.
 		t.Fatal("Expected output configuration to be parsed")
 	}
 
-	if workflowData.SafeOutputs.CreatePullRequest == nil {
+	if workflowData.SafeOutputs.CreatePullRequests == nil {
 		t.Fatal("Expected pull-request configuration to be parsed")
 	}
 
 	// Verify title prefix
 	expectedPrefix := "[agent] "
-	if workflowData.SafeOutputs.CreatePullRequest.TitlePrefix != expectedPrefix {
-		t.Errorf("Expected title prefix '%s', got '%s'", expectedPrefix, workflowData.SafeOutputs.CreatePullRequest.TitlePrefix)
+	if workflowData.SafeOutputs.CreatePullRequests.TitlePrefix != expectedPrefix {
+		t.Errorf("Expected title prefix '%s', got '%s'", expectedPrefix, workflowData.SafeOutputs.CreatePullRequests.TitlePrefix)
 	}
 
 	// Verify labels
 	expectedLabels := []string{"automation", "bot"}
-	if len(workflowData.SafeOutputs.CreatePullRequest.Labels) != len(expectedLabels) {
-		t.Errorf("Expected %d labels, got %d", len(expectedLabels), len(workflowData.SafeOutputs.CreatePullRequest.Labels))
+	if len(workflowData.SafeOutputs.CreatePullRequests.Labels) != len(expectedLabels) {
+		t.Errorf("Expected %d labels, got %d", len(expectedLabels), len(workflowData.SafeOutputs.CreatePullRequests.Labels))
 	}
 
 	for i, expectedLabel := range expectedLabels {
-		if i >= len(workflowData.SafeOutputs.CreatePullRequest.Labels) || workflowData.SafeOutputs.CreatePullRequest.Labels[i] != expectedLabel {
-			t.Errorf("Expected label[%d] to be '%s', got '%s'", i, expectedLabel, workflowData.SafeOutputs.CreatePullRequest.Labels[i])
+		if i >= len(workflowData.SafeOutputs.CreatePullRequests.Labels) || workflowData.SafeOutputs.CreatePullRequests.Labels[i] != expectedLabel {
+			t.Errorf("Expected label[%d] to be '%s', got '%s'", i, expectedLabel, workflowData.SafeOutputs.CreatePullRequests.Labels[i])
 		}
 	}
 }
diff --git a/pkg/workflow/plural_safe_outputs_integration_test.go b/pkg/workflow/plural_safe_outputs_integration_test.go
new file mode 100644
index 00000000000..3d21f6c01dd
--- /dev/null
+++ b/pkg/workflow/plural_safe_outputs_integration_test.go
@@ -0,0 +1,213 @@
+package workflow
+
+import (
+	"testing"
+
+	"github.com/githubnext/gh-aw/pkg/parser"
+)
+
+func TestSafeOutputsBackwardCompatibility(t *testing.T) {
+	compiler := &Compiler{}
+
+	t.Run("Legacy singular syntax should still work", func(t *testing.T) {
+		content := `---
+safe-outputs:
+  create-issue:
+    title-prefix: "[Auto] "
+    labels: ["bug", "auto-generated"]
+  add-issue-comment:
+  create-pull-request:
+    title-prefix: "[Fix] "
+    draft: true
+---
+
+# Test workflow
+
+This workflow should work with legacy syntax.
+`
+
+		// Parse the workflow content
+		result, err := parser.ExtractFrontmatterFromContent(content)
+		if err != nil {
+			t.Fatalf("Failed to parse frontmatter: %v", err)
+		}
+
+		config := compiler.extractSafeOutputsConfig(result.Frontmatter)
+		if config == nil {
+			t.Fatal("Expected config to be parsed")
+		}
+
+		// Verify create-issue (singular) is converted to create-issues with max: 1
+		if config.CreateIssues == nil {
+			t.Fatal("Expected CreateIssues to be parsed from legacy create-issue")
+		}
+		if config.CreateIssues.Max != 1 {
+			t.Errorf("Expected CreateIssues.Max to be 1 for legacy syntax, got %d", config.CreateIssues.Max)
+		}
+		if config.CreateIssues.TitlePrefix != "[Auto] " {
+			t.Errorf("Expected TitlePrefix '[Auto] ', got '%s'", config.CreateIssues.TitlePrefix)
+		}
+		if len(config.CreateIssues.Labels) != 2 {
+			t.Errorf("Expected 2 labels, got %d", len(config.CreateIssues.Labels))
+		}
+
+		// Verify add-issue-comment (singular) is converted to add-issue-comments with max: 1
+		if config.AddIssueComments == nil {
+			t.Fatal("Expected AddIssueComments to be parsed from legacy add-issue-comment")
+		}
+		if config.AddIssueComments.Max != 1 {
+			t.Errorf("Expected AddIssueComments.Max to be 1 for legacy syntax, got %d", config.AddIssueComments.Max)
+		}
+
+		// Verify create-pull-request (singular) stays as max: 1
+		if config.CreatePullRequests == nil {
+			t.Fatal("Expected CreatePullRequests to be parsed from create-pull-request")
+		}
+		if config.CreatePullRequests.Max != 1 {
+			t.Errorf("Expected CreatePullRequests.Max to be 1 for singular syntax, got %d", config.CreatePullRequests.Max)
+		}
+		if config.CreatePullRequests.TitlePrefix != "[Fix] " {
+			t.Errorf("Expected TitlePrefix '[Fix] ', got '%s'", config.CreatePullRequests.TitlePrefix)
+		}
+		if config.CreatePullRequests.Draft == nil || *config.CreatePullRequests.Draft != true {
+			t.Errorf("Expected Draft to be true, got %v", config.CreatePullRequests.Draft)
+		}
+	})
+
+	t.Run("New plural syntax should work", func(t *testing.T) {
+		content := `---
+safe-outputs:
+  create-issues:
+    title-prefix: "[Batch] "
+    labels: ["enhancement"]
+    max: 5
+  add-issue-comments:
+    max: 3
+  create-pull-request:
+    title-prefix: "[Single] "
+    draft: false
+---
+
+# Test workflow
+
+This workflow uses the new plural syntax for issues and comments, singular for pull requests.
+`
+
+		// Parse the workflow content
+		result, err := parser.ExtractFrontmatterFromContent(content)
+		if err != nil {
+			t.Fatalf("Failed to parse frontmatter: %v", err)
+		}
+
+		config := compiler.extractSafeOutputsConfig(result.Frontmatter)
+		if config == nil {
+			t.Fatal("Expected config to be parsed")
+		}
+
+		// Verify create-issues (plural) with explicit max
+		if config.CreateIssues == nil {
+			t.Fatal("Expected CreateIssues to be parsed")
+		}
+		if config.CreateIssues.Max != 5 {
+			t.Errorf("Expected CreateIssues.Max to be 5, got %d", config.CreateIssues.Max)
+		}
+		if config.CreateIssues.TitlePrefix != "[Batch] " {
+			t.Errorf("Expected TitlePrefix '[Batch] ', got '%s'", config.CreateIssues.TitlePrefix)
+		}
+
+		// Verify add-issue-comments (plural) with explicit max
+		if config.AddIssueComments == nil {
+			t.Fatal("Expected AddIssueComments to be parsed")
+		}
+		if config.AddIssueComments.Max != 3 {
+			t.Errorf("Expected AddIssueComments.Max to be 3, got %d", config.AddIssueComments.Max)
+		}
+
+		// Verify create-pull-request (singular) is always max: 1
+		if config.CreatePullRequests == nil {
+			t.Fatal("Expected CreatePullRequests to be parsed")
+		}
+		if config.CreatePullRequests.Max != 1 {
+			t.Errorf("Expected CreatePullRequests.Max to be 1 (singular), got %d", config.CreatePullRequests.Max)
+		}
+		if config.CreatePullRequests.Draft == nil || *config.CreatePullRequests.Draft != false {
+			t.Errorf("Expected Draft to be false, got %v", config.CreatePullRequests.Draft)
+		}
+	})
+
+	t.Run("Plural syntax without explicit max should default to 10", func(t *testing.T) {
+		content := `---
+safe-outputs:
+  create-issues:
+  add-issue-comments:
+  create-pull-request:
+---
+
+# Test workflow
+
+This workflow uses plural syntax without explicit max values (except pull request which is always singular).
+`
+
+		// Parse the workflow content
+		result, err := parser.ExtractFrontmatterFromContent(content)
+		if err != nil {
+			t.Fatalf("Failed to parse frontmatter: %v", err)
+		}
+
+		config := compiler.extractSafeOutputsConfig(result.Frontmatter)
+		if config == nil {
+			t.Fatal("Expected config to be parsed")
+		}
+
+		// Issues and comments should default to max: 10, pull requests always max: 1
+		if config.CreateIssues == nil || config.CreateIssues.Max != 10 {
+			t.Errorf("Expected CreateIssues.Max to be 10, got %d", config.CreateIssues.Max)
+		}
+		if config.AddIssueComments == nil || config.AddIssueComments.Max != 10 {
+			t.Errorf("Expected AddIssueComments.Max to be 10, got %d", config.AddIssueComments.Max)
+		}
+		if config.CreatePullRequests == nil || config.CreatePullRequests.Max != 1 {
+			t.Errorf("Expected CreatePullRequests.Max to be 1 (singular), got %d", config.CreatePullRequests.Max)
+		}
+	})
+}
+
+func TestWorkflowCompilationWithPluralSafeOutputs(t *testing.T) {
+	t.Run("Workflow should parse plural safe-outputs configuration", func(t *testing.T) {
+		content := `---
+safe-outputs:
+  create-issues:
+    max: 2
+  add-issue-comments:
+    max: 4
+---
+
+# Test workflow
+
+This workflow uses plural safe-outputs and should compile successfully.
+
+Analyze the repository and create issues for any problems found.
+`
+
+		// Parse just the frontmatter content
+		result, err := parser.ExtractFrontmatterFromContent(content)
+		if err != nil {
+			t.Fatalf("Failed to parse frontmatter: %v", err)
+		}
+
+		// Extract safe-outputs configuration
+		compiler := &Compiler{}
+		config := compiler.extractSafeOutputsConfig(result.Frontmatter)
+
+		// Verify the configuration was parsed correctly
+		if config == nil {
+			t.Fatal("Expected SafeOutputs to be parsed")
+		}
+		if config.CreateIssues == nil || config.CreateIssues.Max != 2 {
+			t.Errorf("Expected CreateIssues.Max to be 2, got %v", config.CreateIssues)
+		}
+		if config.AddIssueComments == nil || config.AddIssueComments.Max != 4 {
+			t.Errorf("Expected AddIssueComments.Max to be 4, got %v", config.AddIssueComments)
+		}
+	})
+}
diff --git a/pkg/workflow/plural_safe_outputs_test.go b/pkg/workflow/plural_safe_outputs_test.go
new file mode 100644
index 00000000000..5f242fad663
--- /dev/null
+++ b/pkg/workflow/plural_safe_outputs_test.go
@@ -0,0 +1,206 @@
+package workflow
+
+import (
+	"testing"
+)
+
+func TestPluralSafeOutputs(t *testing.T) {
+	compiler := &Compiler{}
+
+	t.Run("Singular forms should convert to max: 1", func(t *testing.T) {
+		testSingular := map[string]any{
+			"safe-outputs": map[string]any{
+				"create-issue":        nil,
+				"add-issue-comment":   nil,
+				"create-pull-request": nil,
+			},
+		}
+
+		config := compiler.extractSafeOutputsConfig(testSingular)
+		if config == nil {
+			t.Fatal("Expected config to be parsed")
+		}
+
+		if config.CreateIssues == nil {
+			t.Fatal("Expected CreateIssues to be parsed")
+		}
+		if config.CreateIssues.Max != 1 {
+			t.Errorf("Expected CreateIssues.Max to be 1 for singular form, got %d", config.CreateIssues.Max)
+		}
+
+		if config.AddIssueComments == nil {
+			t.Fatal("Expected AddIssueComments to be parsed")
+		}
+		if config.AddIssueComments.Max != 1 {
+			t.Errorf("Expected AddIssueComments.Max to be 1 for singular form, got %d", config.AddIssueComments.Max)
+		}
+
+		if config.CreatePullRequests == nil {
+			t.Fatal("Expected CreatePullRequests to be parsed")
+		}
+		if config.CreatePullRequests.Max != 1 {
+			t.Errorf("Expected CreatePullRequests.Max to be 1 for singular form, got %d", config.CreatePullRequests.Max)
+		}
+	})
+
+	t.Run("Plural forms should default to max: 10", func(t *testing.T) {
+		testPlural := map[string]any{
+			"safe-outputs": map[string]any{
+				"create-issues":       nil,
+				"add-issue-comments":  nil,
+				"create-pull-request": nil, // Note: singular, not plural
+			},
+		}
+
+		config := compiler.extractSafeOutputsConfig(testPlural)
+		if config == nil {
+			t.Fatal("Expected config to be parsed")
+		}
+
+		if config.CreateIssues == nil {
+			t.Fatal("Expected CreateIssues to be parsed")
+		}
+		if config.CreateIssues.Max != 10 {
+			t.Errorf("Expected CreateIssues.Max to be 10 for plural form, got %d", config.CreateIssues.Max)
+		}
+
+		if config.AddIssueComments == nil {
+			t.Fatal("Expected AddIssueComments to be parsed")
+		}
+		if config.AddIssueComments.Max != 10 {
+			t.Errorf("Expected AddIssueComments.Max to be 10 for plural form, got %d", config.AddIssueComments.Max)
+		}
+
+		if config.CreatePullRequests == nil {
+			t.Fatal("Expected CreatePullRequests to be parsed")
+		}
+		if config.CreatePullRequests.Max != 1 {
+			t.Errorf("Expected CreatePullRequests.Max to be 1 for singular form, got %d", config.CreatePullRequests.Max)
+		}
+	})
+
+	t.Run("Plural forms with explicit max should use provided value", func(t *testing.T) {
+		testPluralMax := map[string]any{
+			"safe-outputs": map[string]any{
+				"create-issues": map[string]any{
+					"max": 3,
+				},
+				"add-issue-comments": map[string]any{
+					"max": 5,
+				},
+				"create-pull-request": map[string]any{
+					// max parameter is ignored for pull requests
+					"max": 2,
+				},
+			},
+		}
+
+		config := compiler.extractSafeOutputsConfig(testPluralMax)
+		if config == nil {
+			t.Fatal("Expected config to be parsed")
+		}
+
+		if config.CreateIssues == nil {
+			t.Fatal("Expected CreateIssues to be parsed")
+		}
+		if config.CreateIssues.Max != 3 {
+			t.Errorf("Expected CreateIssues.Max to be 3, got %d", config.CreateIssues.Max)
+		}
+
+		if config.AddIssueComments == nil {
+			t.Fatal("Expected AddIssueComments to be parsed")
+		}
+		if config.AddIssueComments.Max != 5 {
+			t.Errorf("Expected AddIssueComments.Max to be 5, got %d", config.AddIssueComments.Max)
+		}
+
+		if config.CreatePullRequests == nil {
+			t.Fatal("Expected CreatePullRequests to be parsed")
+		}
+		if config.CreatePullRequests.Max != 1 {
+			t.Errorf("Expected CreatePullRequests.Max to be 1 (max ignored for pull requests), got %d", config.CreatePullRequests.Max)
+		}
+	})
+
+	t.Run("Mixed configurations should work correctly", func(t *testing.T) {
+		testMixed := map[string]any{
+			"safe-outputs": map[string]any{
+				"create-issues": map[string]any{
+					"title-prefix": "[Auto] ",
+					"labels":       []any{"bug", "auto-generated"},
+					"max":          2,
+				},
+				"create-pull-request": map[string]any{
+					"title-prefix": "[Fix] ",
+					"labels":       []any{"fix"},
+					"draft":        true,
+				},
+			},
+		}
+
+		config := compiler.extractSafeOutputsConfig(testMixed)
+		if config == nil {
+			t.Fatal("Expected config to be parsed")
+		}
+
+		// Check plural create-issues
+		if config.CreateIssues == nil {
+			t.Fatal("Expected CreateIssues to be parsed")
+		}
+		if config.CreateIssues.Max != 2 {
+			t.Errorf("Expected CreateIssues.Max to be 2, got %d", config.CreateIssues.Max)
+		}
+		if config.CreateIssues.TitlePrefix != "[Auto] " {
+			t.Errorf("Expected CreateIssues.TitlePrefix to be '[Auto] ', got '%s'", config.CreateIssues.TitlePrefix)
+		}
+		if len(config.CreateIssues.Labels) != 2 || config.CreateIssues.Labels[0] != "bug" || config.CreateIssues.Labels[1] != "auto-generated" {
+			t.Errorf("Expected CreateIssues.Labels to be ['bug', 'auto-generated'], got %v", config.CreateIssues.Labels)
+		}
+
+		// Check singular create-pull-request (should convert to max: 1)
+		if config.CreatePullRequests == nil {
+			t.Fatal("Expected CreatePullRequests to be parsed")
+		}
+		if config.CreatePullRequests.Max != 1 {
+			t.Errorf("Expected CreatePullRequests.Max to be 1 for singular form, got %d", config.CreatePullRequests.Max)
+		}
+		if config.CreatePullRequests.TitlePrefix != "[Fix] " {
+			t.Errorf("Expected CreatePullRequests.TitlePrefix to be '[Fix] ', got '%s'", config.CreatePullRequests.TitlePrefix)
+		}
+		if len(config.CreatePullRequests.Labels) != 1 || config.CreatePullRequests.Labels[0] != "fix" {
+			t.Errorf("Expected CreatePullRequests.Labels to be ['fix'], got %v", config.CreatePullRequests.Labels)
+		}
+		if config.CreatePullRequests.Draft == nil || *config.CreatePullRequests.Draft != true {
+			t.Errorf("Expected CreatePullRequests.Draft to be true, got %v", config.CreatePullRequests.Draft)
+		}
+	})
+
+	t.Run("Should prefer plural form when both singular and plural are present", func(t *testing.T) {
+		testBoth := map[string]any{
+			"safe-outputs": map[string]any{
+				"create-issue": map[string]any{
+					"title-prefix": "[Singular] ",
+				},
+				"create-issues": map[string]any{
+					"title-prefix": "[Plural] ",
+					"max":          5,
+				},
+			},
+		}
+
+		config := compiler.extractSafeOutputsConfig(testBoth)
+		if config == nil {
+			t.Fatal("Expected config to be parsed")
+		}
+
+		if config.CreateIssues == nil {
+			t.Fatal("Expected CreateIssues to be parsed")
+		}
+		if config.CreateIssues.Max != 5 {
+			t.Errorf("Expected CreateIssues.Max to be 5 (from plural form), got %d", config.CreateIssues.Max)
+		}
+		if config.CreateIssues.TitlePrefix != "[Plural] " {
+			t.Errorf("Expected CreateIssues.TitlePrefix to be '[Plural] ' (from plural form), got '%s'", config.CreateIssues.TitlePrefix)
+		}
+	})
+}
