Aller au contenu

Generating LinkedIn Posts with AI for Sales Reps Covering Multiple Industries

Each of our sales reps covers several industries, and each industry has a different target audience (IT leadership, marketing, general management) with a pitch that should vary accordingly. Nobody has time to keep up that pace of personalization on top of actual prospecting: here's how an n8n pipeline generates a weekly base of ready-to-edit LinkedIn posts every week, sorted by industry and by target audience; and why the real difficulty wasn't wiring an LLM to a prompt.


The problem

On LinkedIn, a post that resonates with a CIO at an insurance company has nothing to do with a post aimed at the marketing leadership of a software vendor. In practice, nobody has time to keep up that pace of industry-specific personalization on top of actual prospecting, so LinkedIn posts either get skipped entirely or end up as generic shares.

The starting idea was simple: build on what we already know (each sales rep's industry and target audience, the product documentation) and let an AI agent produce a weekly base of ready-to-edit posts every week, sorted by industry and by target audience. Not a final post to publish as-is: a qualified raw material, so nobody starts from a blank page.

Field note: the real difficulty wasn't wiring an LLM to a prompt. It was building the right data upstream (who targets what, with which arguments), and avoiding a result that reads like a disguised prospecting email rather than an actual social media post.


Architecture & tech stack

Everything runs in n8n, with the following building blocks:

  • n8n Data Tables to store two datasets: the sales rep × industry × target mapping (with the associated customer references and product arguments), and a condensed product fact base extracted from internal documentation.
  • An AI agent (n8n's LangChain Agent node) wired to OpenRouter, with forced structured output (Structured Output Parser) to get directly usable JSON rather than a block of text to parse by hand.
  • A Code node to group the generated posts by sales rep and build a summary .html file.
  • SharePoint to host the weekly file.
  • Microsoft Teams to notify each sales rep with a private message containing the link to their file.
flowchart TD
    A["Weekly trigger\nMonday 7am"] --> B["Data Table\nproduct facts"]
    B --> C["Data Table\nsales rep x industry x target"]
    C --> D{"Loop\n1 row at a time"}
    D -->|for each row| E["AI Agent\nOpenRouter + structured output"]
    E --> F["Merge row + generated posts"]
    F --> D
    D -->|loop finished| G["Group by sales rep"]
    G --> H["Generate .html file"]
    H --> I["Upload to SharePoint"]
    I --> J{"Switch: which sales rep?"}
    J --> K1["Teams 1:1 Sales Rep A"]
    J --> K2["Teams 1:1 Sales Rep B"]
    J --> K3["Teams 1:1 Sales Rep C"]
    J --> K4["Teams 1:1 Sales Rep D"]

The choice of AI model shifted along the way: started with a "flagship" model for quality, then switched back to a much lighter model after a first test: the quality/price ratio didn't justify the gap for a short copywriting task (more on that below).


Building it step by step

1. Mapping the existing data

Before writing a single line of workflow, I had to look at what we already had: a sales rep profile file (target industry, target job title, company size, customer references, product arguments) built earlier for prospect sourcing. This file already existed at a fine level of granularity: the same industry could appear several times depending on whether the target was IT or marketing. A natural instinct would have been to "deduplicate" these rows to simplify things; in reality they represent genuinely different pitches, so they were all kept as separate entries.

2. Condensing the product documentation into verifiable facts

To keep the LLM from inventing features or numbers, a read-through of the product documentation produced about fifty short, verifiable facts (a feature, a technical characteristic, a specific figure), sorted by theme. This base acts as a factual guardrail injected into the system prompt, rather than letting the model "improvise" from its general knowledge of the product.

Example of an extracted fact (anonymized):

{
  "theme": "scenarios",
  "fait": "Scenarios can be triggered by a form (on submission) or by targeting (segment recalculated daily to include new contacts)."
}

3. Building the Data Tables

n8n recently introduced native Data Tables (no need for an external database at this volume). Two tables were enough:

Table Columns Volume
commercial_secteurs commercial, secteur, cible, taille_entreprise, zone_geo, references_client, avantages_campaign ~110 rows
produit_faits theme, fait ~50 rows

4. The generation pipeline

The core of the workflow loops over each row of commercial_secteurs (a "Split In Batches" loop, batch size 1) and calls an AI agent with:

  • a fixed system prompt that carries all the style constraints (see below),
  • a dynamic user prompt, filled with the fields from the current row,
  • forced structured output, to get { "posts": [...] } directly, with an angle and a text for each proposal.

Excerpt from the system prompt (translated from the actual constraints, with no internal data):

Write in French, in the first person singular, from the sales rep's point of view,
like a real public LinkedIn post (experience, opinion, field observation) - never
like a prospecting message or an email addressed to a single recipient.

Never address a specific person formally. Ban phrasing like "I wanted to bring to
your attention...", "would you like me to show you...".

Generate exactly 3 proposals, with different angles: social proof (customer
reference), industry insight (business pain point), product news (feature).

5. Grouping and formatting

Once the loop has run through every row, a JavaScript code node groups the results by sales rep (one input item per industry row, one output item per sales rep, with the list of their proposals):

const groups = {};
for (const item of items) {
  const c = item.json.commercial;
  if (!groups[c]) groups[c] = [];
  groups[c].push({ secteur: item.json.secteur, posts: item.json.posts /* ... */ });
}
return Object.keys(groups).map((commercial) => ({
  json: { commercial, entries: groups[commercial] }
}));

A second code node then builds a standalone .html page per sales rep (one section per industry, with the 3 post proposals inside), converted to a binary file for upload.

6. Distribution: SharePoint + Teams per branch

Rather than one giant Teams message listing every industry (unreadable for sales reps covering thirty of them), the .html file is dropped onto a shared SharePoint space, and only the link is sent over Teams. For sending, a Switch routes each item to a dedicated Teams node per sales rep (an existing 1:1 chat), rather than a single generic node, reusing a pattern already used in another internal workflow for daily task reminders.


What broke (and what it teaches)

The field that disappears after an API call

A Switch that stopped routing anything

First bug of the first real test: the Switch routed zero items across its 4 branches, even though it was clearly receiving the right items as input. The cause: the SharePoint upload node replaces each item's content with the API response (file ID, link, metadata); so the commercial field the Switch used for routing no longer existed at that point in the pipeline.

The general lesson, valid for any tool of this kind: as soon as a node calls an external API, assume it replaces the item's data rather than enriching it. The fix is to explicitly reference the earlier node that still carried the right value, instead of blindly reading "the current item".

The "disguised prospecting message" tone

Second surprise, and a more interesting one: the first generated posts all ended with phrasing like "would you like me to show you an example tailored to your catalog?". Technically correct, but they read like a private message to a specific prospect, not a public post addressed to a network. Digging in, the cause was in the prompt itself: the instruction asked for an "invitation to continue the conversation privately" at the end of the post, which mechanically pushes the model toward a 1:1 outreach tone.

Fix: explicitly ban prospecting phrasing in the system prompt, and replace the closing instruction with either an open question addressed to the whole network, or no question at all: a post that ends on a strong observation works just as well.

The model cost

First test on a "flagship" model: about $0.037 per call. Scaled up to the full weekly run (about a hundred rows), that added up to a non-trivial monthly bill for a copywriting task that doesn't need heavy reasoning. Switching to a much more affordable model from the same provider: equivalent perceived quality for this use case, cost divided by several times over.


Current limitations and possible improvements

  • No feedback loop: the generated posts aren't rated or reused to refine the prompt over time. A future iteration could store a simple thumbs up/down per post to adjust the instructions.
  • No content rotation: the pipeline regenerates everything from scratch every week with no memory of what's already been proposed, with a risk of repetition on recurring industries.
  • Mandatory human review: this is a deliberate choice (actually publishing the messages stays manual), but it means the time savings depend entirely on adoption by the sales reps.
  • HTML rendering robustness in Teams: HTML rendering in Teams messages is still limited to a subset of tags; sending a link instead of the full inline content solves the problem for now, but a proper dedicated viewer would be more comfortable long term.

FAQ

Why not put everything in a single Teams message? Because some sales reps cover more than 30 industry/target combinations: a message listing everything would become unreadable in a chat bubble. A separate, browsable file with just the link sent over is far more comfortable.

Why structured output instead of parsing free text afterward? Because parsing free text to extract 3 separate posts is fragile (inconsistent formats, missing separators). Forcing a JSON schema at generation time eliminates that whole class of bugs.

Is the generated content publishable as-is? No, and that's not the goal. It's a qualified working base by industry and by target, not a final post: human review and personalization remain necessary.

Conclusion

The project confirms an intuition already proven on other automations: the AI part (the prompt, the model) is almost never the part that takes the most time. What takes time is structuring the data upstream so it's usable, and iterating on the prompt once the first results are in hand to fix biases we hadn't anticipated: here, a prospecting tone that crept into content meant to be public. The pipeline is now running for two sales reps in testing ahead of a full rollout.


On the same ground (AI agent + structured output + n8n), see also Automating a Grammar Variable with AI, where the same forced-output principle serves as a quality guardrail this time, rather than a formatting one.