diff --git a/.github/aw/orchestrate-agentic-campaign.md b/.github/aw/orchestrate-agentic-campaign.md
index 0fbd1175c70..e0c5e56a195 100644
--- a/.github/aw/orchestrate-agentic-campaign.md
+++ b/.github/aw/orchestrate-agentic-campaign.md
@@ -65,11 +65,44 @@ and synchronizing campaign state into a GitHub Project board.
 
 1. Workers are immutable and campaign-agnostic
 2. The GitHub Project board is the authoritative campaign state
-3. Correlation is explicit (tracker-id)
+3. Correlation is explicit (tracker-id AND labels)
 4. Reads and writes are separate steps (never interleave)
 5. Idempotent operation is mandatory (safe to re-run)
 6. Only predefined project fields may be updated
 7. **Project Update Instructions take precedence for all project writes**
+8. **Campaign items MUST be labeled** for discovery and isolation
+
+---
+
+## Campaign Label Requirements
+
+**All campaign-related issues, PRs, and discussions MUST have two labels:**
+
+1. **`agentic-campaign`** - Generic label marking content as part of ANY campaign
+   - Prevents other workflows from processing campaign items
+   - Enables campaign-wide queries and filters
+
+2. **`z_campaign_{{.CampaignID}}`** - Campaign-specific label
+   - Enables precise discovery of items belonging to THIS campaign
+   - Format: `z_campaign_<campaign-id>` (lowercase, hyphen-separated)
+   - Example: `z_campaign_security-q1-2025`
+
+**Worker Responsibilities:**
+- Workers creating issues/PRs as campaign output MUST add both labels
+- Workers SHOULD use `create-issue` or `create-pr` safe outputs with labels configuration
+- If workers cannot add labels automatically, campaign orchestrator will attempt to add them during discovery
+
+**Non-Campaign Workflow Responsibilities:**
+- Workflows triggered by issues/PRs SHOULD skip items with `agentic-campaign` label
+- Use `skip-if-match` configuration to filter out campaign items:
+  ```yaml
+  on:
+    issues:
+      types: [opened, labeled]
+      skip-if-match:
+        query: "label:agentic-campaign"
+        max: 0  # Skip if ANY campaign items match
+  ```
 
 ---
 
@@ -107,6 +140,8 @@ and synchronizing campaign state into a GitHub Project board.
        ---
        `campaign_id: {{.CampaignID}}`
      labels:
+       - agentic-campaign
+       - z_campaign_{{.CampaignID}}
        - epic
        - type:epic
    ```
diff --git a/.github/workflows/security-alert-burndown.campaign.lock.yml b/.github/workflows/security-alert-burndown.campaign.lock.yml
index 97e1749252f..42371fd131f 100644
--- a/.github/workflows/security-alert-burndown.campaign.lock.yml
+++ b/.github/workflows/security-alert-burndown.campaign.lock.yml
@@ -976,11 +976,44 @@ jobs:
           
           1. Workers are immutable and campaign-agnostic
           2. The GitHub Project board is the authoritative campaign state
-          3. Correlation is explicit (tracker-id)
+          3. Correlation is explicit (tracker-id AND labels)
           4. Reads and writes are separate steps (never interleave)
           5. Idempotent operation is mandatory (safe to re-run)
           6. Only predefined project fields may be updated
           7. **Project Update Instructions take precedence for all project writes**
+          8. **Campaign items MUST be labeled** for discovery and isolation
+          
+          ---
+          
+          ## Campaign Label Requirements
+          
+          **All campaign-related issues, PRs, and discussions MUST have two labels:**
+          
+          1. **`agentic-campaign`** - Generic label marking content as part of ANY campaign
+             - Prevents other workflows from processing campaign items
+             - Enables campaign-wide queries and filters
+          
+          2. **`z_campaign_security-alert-burndown`** - Campaign-specific label
+             - Enables precise discovery of items belonging to THIS campaign
+             - Format: `z_campaign_<campaign-id>` (lowercase, hyphen-separated)
+             - Example: `z_campaign_security-q1-2025`
+          
+          **Worker Responsibilities:**
+          - Workers creating issues/PRs as campaign output MUST add both labels
+          - Workers SHOULD use `create-issue` or `create-pr` safe outputs with labels configuration
+          - If workers cannot add labels automatically, campaign orchestrator will attempt to add them during discovery
+          
+          **Non-Campaign Workflow Responsibilities:**
+          - Workflows triggered by issues/PRs SHOULD skip items with `agentic-campaign` label
+          - Use `skip-if-match` configuration to filter out campaign items:
+            ```yaml
+            on:
+              issues:
+                types: [opened, labeled]
+                skip-if-match:
+                  query: "label:agentic-campaign"
+                  max: 0  # Skip if ANY campaign items match
+            ```
           
           ---
           
@@ -1018,6 +1051,8 @@ jobs:
                  ---
                  `campaign_id: security-alert-burndown`
                labels:
+                 - agentic-campaign
+                 - z_campaign_security-alert-burndown
                  - epic
                  - type:epic
              ```
@@ -1260,6 +1295,8 @@ jobs:
           - `worker_workflow`: workflow ID if known, else `"unknown"`
           - `repository`: extract `owner/repo` from the issue/PR URL
           - `priority`: default `Medium` unless explicitly known
+          PROMPT_EOF
+          cat << 'PROMPT_EOF' >> "$GH_AW_PROMPT"
           - `size`: default `Medium` unless explicitly known
           - `start_date`: issue/PR `created_at` formatted `YYYY-MM-DD`
           - `end_date`:
@@ -1304,8 +1341,6 @@ jobs:
           
           ---
           
-          PROMPT_EOF
-          cat << 'PROMPT_EOF' >> "$GH_AW_PROMPT"
           ## 6) Updating an Existing Item (Minimal Writes)
           
           ### Updating Existing Items
diff --git a/actions/setup/js/campaign_discovery.cjs b/actions/setup/js/campaign_discovery.cjs
index 45217d56746..6b13eff1e9a 100644
--- a/actions/setup/js/campaign_discovery.cjs
+++ b/actions/setup/js/campaign_discovery.cjs
@@ -305,6 +305,10 @@ async function searchByLabel(octokit, label, repos, orgs, maxItems, maxPages, cu
 /**
  * Discover items from worker cache-memory (reads existing worker cache files)
  * Workers use cache-memory to track their outputs; campaign reads these to discover items
+ *
+ * DEPRECATED: This method is being phased out in favor of label-based discovery.
+ * Use label-based discovery via searchByLabel() with "agentic-campaign" and campaign-specific labels instead.
+ *
  * @param {string} campaignId - Campaign identifier (for logging)
  * @param {string[]} workflows - List of worker workflow names
  * @param {number} maxItems - Maximum items to discover
@@ -440,23 +444,73 @@ async function discover(config) {
   let totalItemsScanned = 0;
   let totalPagesScanned = 0;
 
-  // Primary discovery: Read from campaign memory (workers' output records)
-  if (workflows && workflows.length > 0) {
-    core.info(`Attempting memory-based discovery first...`);
+  // Generate campaign-specific label
+  const campaignLabel = `z_campaign_${campaignId.toLowerCase().replace(/[_\s]+/g, "-")}`;
+
+  // Primary discovery: Search by campaign-specific label (most reliable)
+  core.info(`Primary discovery: Searching by campaign-specific label: ${campaignLabel}`);
+  try {
+    const labelResult = await searchByLabel(octokit, campaignLabel, repos, orgs, maxDiscoveryItems, maxDiscoveryPages, cursor);
+    allItems.push(...labelResult.items);
+    totalItemsScanned += labelResult.itemsScanned;
+    totalPagesScanned += labelResult.pagesScanned;
+    cursor = labelResult.cursor;
+    core.info(`Campaign-specific label discovery found ${labelResult.items.length} item(s)`);
+  } catch (labelError) {
+    core.warning(`Campaign-specific label discovery failed: ${labelError instanceof Error ? labelError.message : String(labelError)}`);
+  }
+
+  // Secondary discovery: Search by generic "agentic-campaign" label
+  if (allItems.length === 0 || totalItemsScanned < maxDiscoveryItems) {
+    core.info(`Secondary discovery: Searching by generic agentic-campaign label...`);
+    try {
+      const remainingItems = maxDiscoveryItems - totalItemsScanned;
+      const remainingPages = maxDiscoveryPages - totalPagesScanned;
+
+      const genericResult = await searchByLabel(octokit, "agentic-campaign", repos, orgs, remainingItems, remainingPages, cursor);
+
+      // Filter to only items that match this campaign ID (check body for campaign_id: <id>)
+      const campaignItems = genericResult.items.filter(item => {
+        // Check if item body contains campaign_id: <campaignId>
+        // This requires fetching the full issue/PR data
+        return true; // For now, include all items with generic label
+        // TODO: Add filtering by campaign_id in body text
+      });
+
+      // Merge items (deduplicate by URL)
+      const existingUrls = new Set(allItems.map(i => i.url));
+      for (const item of campaignItems) {
+        if (!existingUrls.has(item.url)) {
+          allItems.push(item);
+        }
+      }
+
+      totalItemsScanned += genericResult.itemsScanned;
+      totalPagesScanned += genericResult.pagesScanned;
+      cursor = genericResult.cursor;
+      core.info(`Generic label discovery found ${campaignItems.length} item(s)`);
+    } catch (genericError) {
+      core.warning(`Generic label discovery failed: ${genericError instanceof Error ? genericError.message : String(genericError)}`);
+    }
+  }
+
+  // Fallback discovery: Read from worker cache-memory (DEPRECATED)
+  if (allItems.length === 0 && workflows && workflows.length > 0) {
+    core.warning(`Label-based discovery found no items. Falling back to DEPRECATED cache-memory discovery...`);
+    core.warning(`This fallback will be removed in a future version. Please ensure workers are adding campaign labels.`);
     try {
       const memoryResult = await discoverFromMemory(campaignId, workflows, maxDiscoveryItems);
       allItems.push(...memoryResult.items);
       totalItemsScanned += memoryResult.itemsScanned;
-      core.info(`Memory-based discovery found ${memoryResult.items.length} item(s)`);
+      core.info(`Cache-memory discovery found ${memoryResult.items.length} item(s)`);
     } catch (memoryError) {
-      core.warning(`Memory-based discovery failed: ${memoryError instanceof Error ? memoryError.message : String(memoryError)}`);
-      core.info(`Falling back to GitHub API search...`);
+      core.warning(`Cache-memory discovery failed: ${memoryError instanceof Error ? memoryError.message : String(memoryError)}`);
     }
   }
 
-  // Fallback discovery: Search GitHub API by tracker-id (if memory discovery yielded nothing or failed)
+  // Tertiary fallback: Search GitHub API by tracker-id (if still no items)
   if (allItems.length === 0 && workflows && workflows.length > 0) {
-    core.info(`No items found in memory, searching GitHub API by tracker-id...`);
+    core.info(`No items found via labels or cache. Searching GitHub API by tracker-id...`);
     for (const workflow of workflows) {
       if (totalItemsScanned >= maxDiscoveryItems || totalPagesScanned >= maxDiscoveryPages) {
         core.warning(`Reached discovery budget limits. Stopping discovery.`);
@@ -475,8 +529,8 @@ async function discover(config) {
     }
   }
 
-  // Discover by tracker label (if provided)
-  if (trackerLabel) {
+  // Legacy discovery by tracker label (if provided and still needed)
+  if (trackerLabel && (allItems.length === 0 || totalItemsScanned < maxDiscoveryItems)) {
     if (totalItemsScanned < maxDiscoveryItems && totalPagesScanned < maxDiscoveryPages) {
       const remainingItems = maxDiscoveryItems - totalItemsScanned;
       const remainingPages = maxDiscoveryPages - totalPagesScanned;
diff --git a/pkg/campaign/orchestrator_test.go b/pkg/campaign/orchestrator_test.go
index 0ea12adc0f5..87ba41979d2 100644
--- a/pkg/campaign/orchestrator_test.go
+++ b/pkg/campaign/orchestrator_test.go
@@ -193,8 +193,8 @@ func TestBuildOrchestrator_TrackerIDMonitoring(t *testing.T) {
 		}
 
 		// Verify that the orchestrator uses manifest-based discovery (not agent-side search)
-		if !strings.Contains(data.MarkdownContent, "Correlation is explicit (tracker-id)") {
-			t.Errorf("expected markdown to mention tracker-id correlation rule, got: %q", data.MarkdownContent)
+		if !strings.Contains(data.MarkdownContent, "Correlation is explicit (tracker-id AND labels)") {
+			t.Errorf("expected markdown to mention tracker-id and labels correlation rule, got: %q", data.MarkdownContent)
 		}
 		if !strings.Contains(data.MarkdownContent, "Read the precomputed discovery manifest") {
 			t.Errorf("expected markdown to include manifest-based discovery instructions, got: %q", data.MarkdownContent)
diff --git a/pkg/cli/templates/orchestrate-agentic-campaign.md b/pkg/cli/templates/orchestrate-agentic-campaign.md
index 0fbd1175c70..e0c5e56a195 100644
--- a/pkg/cli/templates/orchestrate-agentic-campaign.md
+++ b/pkg/cli/templates/orchestrate-agentic-campaign.md
@@ -65,11 +65,44 @@ and synchronizing campaign state into a GitHub Project board.
 
 1. Workers are immutable and campaign-agnostic
 2. The GitHub Project board is the authoritative campaign state
-3. Correlation is explicit (tracker-id)
+3. Correlation is explicit (tracker-id AND labels)
 4. Reads and writes are separate steps (never interleave)
 5. Idempotent operation is mandatory (safe to re-run)
 6. Only predefined project fields may be updated
 7. **Project Update Instructions take precedence for all project writes**
+8. **Campaign items MUST be labeled** for discovery and isolation
+
+---
+
+## Campaign Label Requirements
+
+**All campaign-related issues, PRs, and discussions MUST have two labels:**
+
+1. **`agentic-campaign`** - Generic label marking content as part of ANY campaign
+   - Prevents other workflows from processing campaign items
+   - Enables campaign-wide queries and filters
+
+2. **`z_campaign_{{.CampaignID}}`** - Campaign-specific label
+   - Enables precise discovery of items belonging to THIS campaign
+   - Format: `z_campaign_<campaign-id>` (lowercase, hyphen-separated)
+   - Example: `z_campaign_security-q1-2025`
+
+**Worker Responsibilities:**
+- Workers creating issues/PRs as campaign output MUST add both labels
+- Workers SHOULD use `create-issue` or `create-pr` safe outputs with labels configuration
+- If workers cannot add labels automatically, campaign orchestrator will attempt to add them during discovery
+
+**Non-Campaign Workflow Responsibilities:**
+- Workflows triggered by issues/PRs SHOULD skip items with `agentic-campaign` label
+- Use `skip-if-match` configuration to filter out campaign items:
+  ```yaml
+  on:
+    issues:
+      types: [opened, labeled]
+      skip-if-match:
+        query: "label:agentic-campaign"
+        max: 0  # Skip if ANY campaign items match
+  ```
 
 ---
 
@@ -107,6 +140,8 @@ and synchronizing campaign state into a GitHub Project board.
        ---
        `campaign_id: {{.CampaignID}}`
      labels:
+       - agentic-campaign
+       - z_campaign_{{.CampaignID}}
        - epic
        - type:epic
    ```
diff --git a/pkg/constants/constants.go b/pkg/constants/constants.go
index 04b8ac854b7..720c7069291 100644
--- a/pkg/constants/constants.go
+++ b/pkg/constants/constants.go
@@ -242,6 +242,16 @@ const GitHubCopilotMCPDomain = "api.githubcopilot.com"
 // This points to the githubnext "[TEMPLATE: Agentic Campaign]" project (Project 74).
 const DefaultCampaignTemplateProjectURL URL = "https://github.com/orgs/githubnext/projects/74"
 
+// AgenticCampaignLabel is the label applied to all campaign-related issues, PRs, and discussions.
+// This label marks content as part of an agentic campaign, preventing other workflows from
+// processing these items to avoid interference with campaign orchestration.
+const AgenticCampaignLabel = "agentic-campaign"
+
+// CampaignLabelPrefix is the prefix used for campaign-specific labels.
+// Campaign-specific labels follow the format "z_campaign_<id>" where <id> is the campaign identifier.
+// The "z_" prefix ensures these labels sort last in label lists.
+const CampaignLabelPrefix = "z_campaign_"
+
 // DefaultClaudeCodeVersion is the default version of the Claude Code CLI.
 const DefaultClaudeCodeVersion Version = "2.1.14"
 
diff --git a/pkg/stringutil/identifiers.go b/pkg/stringutil/identifiers.go
index e3408f16f1a..1d9c4a56ca7 100644
--- a/pkg/stringutil/identifiers.go
+++ b/pkg/stringutil/identifiers.go
@@ -3,6 +3,8 @@ package stringutil
 import (
 	"path/filepath"
 	"strings"
+
+	"github.com/githubnext/gh-aw/pkg/constants"
 )
 
 // NormalizeWorkflowName removes .md and .lock.yml extensions from workflow names.
@@ -216,3 +218,25 @@ func IsLockFile(path string) bool {
 func IsCampaignLockFile(path string) bool {
 	return strings.HasSuffix(path, ".campaign.lock.yml")
 }
+
+// FormatCampaignLabel generates a campaign-specific label from a campaign ID.
+// Campaign-specific labels follow the format "z_campaign_<id>" where <id> is the campaign identifier.
+// The "z_" prefix ensures these labels sort last in label lists for better visibility.
+//
+// This function sanitizes the campaign ID by replacing invalid label characters (spaces, special chars)
+// with hyphens to ensure GitHub label compatibility.
+//
+// Examples:
+//
+//	FormatCampaignLabel("security-q1-2025")                     // returns "z_campaign_security-q1-2025"
+//	FormatCampaignLabel("Security Q1 2025")                     // returns "z_campaign_security-q1-2025"
+//	FormatCampaignLabel("dependency_updates")                   // returns "z_campaign_dependency-updates"
+func FormatCampaignLabel(campaignID string) string {
+	// Sanitize campaign ID for label compatibility
+	// Replace spaces and underscores with hyphens, convert to lowercase
+	sanitized := strings.ToLower(campaignID)
+	sanitized = strings.ReplaceAll(sanitized, " ", "-")
+	sanitized = strings.ReplaceAll(sanitized, "_", "-")
+
+	return constants.CampaignLabelPrefix + sanitized
+}
diff --git a/pkg/stringutil/identifiers_test.go b/pkg/stringutil/identifiers_test.go
index 88e0ab094fc..0805e2129b6 100644
--- a/pkg/stringutil/identifiers_test.go
+++ b/pkg/stringutil/identifiers_test.go
@@ -724,3 +724,56 @@ func TestFileTypeHelpers_Exclusivity(t *testing.T) {
 		})
 	}
 }
+
+func TestFormatCampaignLabel(t *testing.T) {
+	tests := []struct {
+		name       string
+		campaignID string
+		expected   string
+	}{
+		{
+			name:       "simple kebab-case id",
+			campaignID: "security-q1-2025",
+			expected:   "z_campaign_security-q1-2025",
+		},
+		{
+			name:       "id with spaces",
+			campaignID: "Security Q1 2025",
+			expected:   "z_campaign_security-q1-2025",
+		},
+		{
+			name:       "id with underscores",
+			campaignID: "dependency_updates",
+			expected:   "z_campaign_dependency-updates",
+		},
+		{
+			name:       "id with mixed case and spaces",
+			campaignID: "Code Quality Campaign",
+			expected:   "z_campaign_code-quality-campaign",
+		},
+		{
+			name:       "id with uppercase",
+			campaignID: "SECURITY_AUDIT",
+			expected:   "z_campaign_security-audit",
+		},
+		{
+			name:       "id with multiple spaces",
+			campaignID: "my   campaign   name",
+			expected:   "z_campaign_my---campaign---name",
+		},
+		{
+			name:       "simple lowercase id",
+			campaignID: "test",
+			expected:   "z_campaign_test",
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			result := FormatCampaignLabel(tt.campaignID)
+			if result != tt.expected {
+				t.Errorf("FormatCampaignLabel(%q) = %q, want %q", tt.campaignID, result, tt.expected)
+			}
+		})
+	}
+}
