The Outreach Draft Agent automates the final stage of Credence’s brand-side creator discovery workflow. After a brand shortlists a creator, the Credence frontend sends the campaign brief and selected creator profile to an n8n webhook. The workflow then validates and standardizes fields such as the brand name, campaign objective, target audience, region, language, budget, collaboration format, creator expertise, audience profile, match rationale, and recommended use case. This allows the agent to handle slightly different request formats without breaking.

image.png

The normalized data is passed to an LLM through OpenRouter using n8n’s LangChain-based Basic LLM Chain and OpenRouter Chat Model nodes. The export does not specify the exact underlying model, so it can be configured to use any supported OpenRouter model. The model is instructed to generate a warm, professional, non-spammy outreach message of fewer than 180 words, tailored to the creator’s content themes, audience relevance, campaign fit, and proposed collaboration format. The workflow limits the response to approximately 500 output tokens and requests a structured JSON response containing the generated draft.

A final parsing node removes markdown formatting, extracts the JSON even when the model adds extra text, and falls back to using the raw response when necessary. The cleaned draft is then returned to the Credence application as a JSON webhook response, where the brand can review and edit it before contacting the creator. This supports Credence’s broader MVP journey from campaign brief, creator scoring and comparison to shortlisting and AI-assisted outreach.

Tech Stack

n8n, OpenRouter AI Models, and Javascript logic.

User Prompts

You are the outreach assistant for Credence, an AI-powered LinkedIn creator discovery product.

Credence helps brands discover credible LinkedIn creators by authority, audience fit, engagement quality, and campaign relevance.

You will receive:
1. A campaign brief
2. A selected creator profile

Write a concise, professional outreach message from the brand to the creator.

Campaign brief:
{{ JSON.stringify($json.brief, null, 2) }}

Creator profile:
{{ JSON.stringify($json.creator, null, 2) }}

The message should:
- Sound human, warm, and professional
- Mention the creator’s relevant content themes
- Explain why the brand sees a fit
- Briefly describe the campaign
- Suggest a simple collaboration format
- Avoid exaggerated flattery
- Avoid sounding spammy
- Be suitable for LinkedIn DM or email
- Stay under 180 words

Return only valid JSON in this exact format:

{
  "draft": "message here"
}

Cleaned JSON Code

(Replaced sensitive/public-risk fields including credential IDs/names, Google Sheets document ID and URLs, Slack channel ID/name, webhook IDs, node IDs, workflow/version IDs, and the n8n instance ID.)

{
  "name": "Credence - Generate Outreach Draft",
  "nodes": [
    {
      "parameters": {
        "httpMethod": "POST",
        "path": "locale/generate-outreach",
        "responseMode": "responseNode",
        "options": {}
      },
      "type": "n8n-nodes-base.webhook",
      "typeVersion": 2.1,
      "position": [
        0,
        0
      ],
      "id": "644a99fb-18d3-4bb3-bb26-4348a725a077",
      "name": "Webhook",
      "webhookId": "4284418d-9bc5-4ad5-8957-5cab40332e53"
    },
    {
      "parameters": {
        "jsCode": "let input = $json.body ?? $json;\n\n// Lovable sometimes sends an array payload: [{ brief, creator }]\nif (Array.isArray(input)) {\n  input = input[0] ?? {};\n}\n\n// Sometimes n8n may wrap body again\nif (input.body) {\n  input = Array.isArray(input.body) ? input.body[0] ?? {} : input.body;\n}\n\nconst rawBrief =\n  input.brief ??\n  input.campaignBrief ??\n  input.campaign ??\n  {};\n\nconst rawCreator =\n  input.creator ??\n  input.selectedCreator ??\n  {};\n\n// Normalize brief fields\nconst brief = {\n  ...rawBrief,\n\n  campaignName:\n    rawBrief.campaignName ??\n    rawBrief.name ??\n    rawBrief.title ??\n    \"Untitled campaign\",\n\n  brandName:\n    rawBrief.brandName ??\n    rawBrief.brand ??\n    rawBrief.companyName ??\n    rawBrief.company ??\n    rawBrief.brandCompany ??\n    \"Credence Demo Brand\",\n\n  targetAudience:\n    rawBrief.targetAudience ??\n    rawBrief.audience ??\n    rawBrief.targetCustomer ??\n    \"Relevant professional audience\",\n\n  industry:\n    rawBrief.industry ??\n    rawBrief.niche ??\n    \"B2B\",\n\n  region:\n    rawBrief.region ??\n    rawBrief.location ??\n    \"India\",\n\n  language:\n    rawBrief.language ??\n    rawBrief.languagePreference ??\n    \"English\",\n\n  goal:\n    rawBrief.goal ??\n    rawBrief.campaignGoal ??\n    rawBrief.objective ??\n    rawBrief.primaryGoal ??\n    rawBrief.description ??\n    \"Explore a relevant LinkedIn creator collaboration\",\n\n  budgetRange:\n    rawBrief.budgetRange ??\n    rawBrief.budget ??\n    \"Not specified\",\n\n  collaborationType:\n    rawBrief.collaborationType ??\n    rawBrief.deliverable ??\n    rawBrief.creatorFormat ??\n    \"LinkedIn collaboration\"\n};\n\n// Normalize creator fields\nconst creator = {\n  ...rawCreator,\n\n  name:\n    rawCreator.name ??\n    rawCreator.creatorName ??\n    \"Selected creator\",\n\n  title:\n    rawCreator.title ??\n    rawCreator.role ??\n    \"LinkedIn creator\",\n\n  contentThemes:\n    rawCreator.contentThemes ??\n    rawCreator.themes ??\n    rawCreator.contentPillars ??\n    rawCreator.topics ??\n    [],\n\n  audienceSummary:\n    rawCreator.audienceSummary ??\n    rawCreator.audience ??\n    \"Professional LinkedIn audience\",\n\n  matchReason:\n    rawCreator.matchReason ??\n    rawCreator.explanation ??\n    rawCreator.bestUseCase ??\n    \"The creator appears relevant for this campaign.\",\n\n  recommendedUseCase:\n    rawCreator.recommendedUseCase ??\n    rawCreator.bestUseCase ??\n    \"LinkedIn sponsored post\"\n};\n\n// Convert contentThemes to array if needed\nif (!Array.isArray(creator.contentThemes)) {\n  creator.contentThemes = String(creator.contentThemes || \"\")\n    .split(\",\")\n    .map(item => item.trim())\n    .filter(Boolean);\n}\n\n// If contentThemes is still empty, infer from recentPosts/bestUseCase\nif (creator.contentThemes.length === 0) {\n  const inferredThemes = [];\n\n  if (creator.bestUseCase) inferredThemes.push(creator.bestUseCase);\n  if (creator.explanation) inferredThemes.push(creator.explanation);\n  if (Array.isArray(creator.recentPosts)) {\n    creator.recentPosts.slice(0, 2).forEach(post => {\n      if (post.text) inferredThemes.push(post.text);\n    });\n  }\n\n  creator.contentThemes = inferredThemes.length > 0\n    ? inferredThemes\n    : [\"professional content\", \"LinkedIn thought leadership\"];\n}\n\n// Final safety checks\nif (!brief.brandName) {\n  brief.brandName = \"Credence Demo Brand\";\n}\n\nif (!brief.goal) {\n  brief.goal = \"Explore a relevant LinkedIn creator collaboration\";\n}\n\nif (!creator.name) {\n  creator.name = \"Selected creator\";\n}\n\nreturn [\n  {\n    json: {\n      brief,\n      creator\n    }\n  }\n];"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        224,
        0
      ],
      "id": "d126e840-8e1d-4b3b-8497-b1813c77de82",
      "name": "Validate Input"
    },
    {
      "parameters": {
        "promptType": "define",
        "text": "=You are the outreach assistant for Credence, an AI-powered LinkedIn creator discovery product.\n\nCredence helps brands discover credible LinkedIn creators by authority, audience fit, engagement quality, and campaign relevance.\n\nYou will receive:\n1. A campaign brief\n2. A selected creator profile\n\nWrite a concise, professional outreach message from the brand to the creator.\n\nCampaign brief:\n{{ JSON.stringify($json.brief, null, 2) }}\n\nCreator profile:\n{{ JSON.stringify($json.creator, null, 2) }}\n\nThe message should:\n- Sound human, warm, and professional\n- Mention the creator’s relevant content themes\n- Explain why the brand sees a fit\n- Briefly describe the campaign\n- Suggest a simple collaboration format\n- Avoid exaggerated flattery\n- Avoid sounding spammy\n- Be suitable for LinkedIn DM or email\n- Stay under 180 words\n\nReturn only valid JSON in this exact format:\n\n{\n  \"draft\": \"message here\"\n}",
        "batching": {}
      },
      "type": "@n8n/n8n-nodes-langchain.chainLlm",
      "typeVersion": 1.9,
      "position": [
        448,
        0
      ],
      "id": "9a61d1dc-c0b9-4bb0-b514-e6daf775c02d",
      "name": "Basic LLM Chain"
    },
    {
      "parameters": {
        "options": {
          "maxTokens": 500
        }
      },
      "type": "@n8n/n8n-nodes-langchain.lmChatOpenRouter",
      "typeVersion": 1,
      "position": [
        528,
        224
      ],
      "id": "efcbb44b-a895-416f-b2ab-51b388c817db",
      "name": "OpenRouter Chat Model"
    },
    {
      "parameters": {
        "jsCode": "const rawContent =\n  $json.choices?.[0]?.message?.content ??\n  $json.response?.text ??\n  $json.response ??\n  $json.text ??\n  $json.output ??\n  $json.content ??\n  $json.data ??\n  \"\";\n\nif (!rawContent) {\n  throw new Error(\"LLM returned no readable content. Check the Basic LLM Chain output.\");\n}\n\nlet cleaned =\n  typeof rawContent === \"string\"\n    ? rawContent.trim()\n    : JSON.stringify(rawContent);\n\n// Remove markdown code fences if the model adds them\ncleaned = cleaned\n  .replace(/^```json\\s*/i, \"\")\n  .replace(/^```\\s*/i, \"\")\n  .replace(/```$/i, \"\")\n  .trim();\n\n// Try to extract JSON object even if the model adds surrounding text\nconst jsonMatch = cleaned.match(/\\{[\\s\\S]*\\}/);\nif (jsonMatch) {\n  cleaned = jsonMatch[0];\n}\n\nlet parsed;\n\ntry {\n  parsed = JSON.parse(cleaned);\n} catch (error) {\n  parsed = {\n    draft: cleaned\n  };\n}\n\nif (!parsed.draft) {\n  throw new Error(\"Parsed response does not contain draft\");\n}\n\nreturn [\n  {\n    json: {\n      draft: parsed.draft,\n      source: \"n8n\",\n      workflow: \"generate-outreach\",\n      generatedAt: new Date().toISOString()\n    }\n  }\n];"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        800,
        0
      ],
      "id": "77e0ea56-dbea-44dc-8333-b06dedd45228",
      "name": "Parse Draft Response"
    },
    {
      "parameters": {
        "options": {
          "responseHeaders": {
            "entries": [
              {
                "name": "Content-Type",
                "value": "application/json"
              }
            ]
          }
        }
      },
      "type": "n8n-nodes-base.respondToWebhook",
      "typeVersion": 1.5,
      "position": [
        1024,
        0
      ],
      "id": "bce138f3-eb13-41d2-8db7-dfdb7519e65e",
      "name": "Respond to Webhook"
    }
  ],
  "pinData": {
    "Webhook": [
      {
        "json": {
          "brief": {
            "campaignName": "AI productivity tool launch",
            "brandName": "FlowPilot",
            "targetAudience": "early-stage founders and product managers",
            "industry": "AI SaaS",
            "region": "South India",
            "language": "English",
            "goal": "drive awareness and product trials",
            "budgetRange": "₹50,000–₹1,00,000",
            "collaborationType": "LinkedIn sponsored post"
          },
          "creator": {
            "id": "cr_003",
            "name": "Pranav Iyer",
            "title": "Product Operator",
            "city": "Chennai",
            "region": "South India",
            "language": "English",
            "contentThemes": [
              "AI workflows",
              "product management",
              "startup execution"
            ],
            "audienceSummary": "Product managers, founders, and early-stage operators",
            "matchReason": "Strong fit because his audience overlaps with AI SaaS founders and product managers.",
            "recommendedUseCase": "LinkedIn launch post plus practical workflow breakdown"
          }
        }
      }
    ]
  },
  "connections": {
    "Webhook": {
      "main": [
        [
          {
            "node": "Validate Input",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "OpenRouter Chat Model": {
      "ai_languageModel": [
        [
          {
            "node": "Basic LLM Chain",
            "type": "ai_languageModel",
            "index": 0
          }
        ]
      ]
    },
    "Validate Input": {
      "main": [
        [
          {
            "node": "Basic LLM Chain",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Basic LLM Chain": {
      "main": [
        [
          {
            "node": "Parse Draft Response",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Parse Draft Response": {
      "main": [
        [
          {
            "node": "Respond to Webhook",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "active": false,
  "settings": {
    "executionOrder": "v1",
    "binaryMode": "separate",
    "availableInMCP": false
  },
  "nodeGroups": [],
  "tags": []
}