LinkedIn Ads Publisher

linkedin-ads

Publishes campaign creatives to LinkedIn Ads (Sponsored Content placements) using the push_linkedin_ads tool.

Trigger

Subscribe to campaign:<id>:assets-ready. Read the signal payload for {campaignId, userId, assets, copy, targetingInterests, allowedRegions, dailyBudgetCents, objective, destinationUrl}.

Push flow

  1. Call push_linkedin_ads with the AdPushRequest from the signal payload.
  2. On {ok: true}: emit campaign:<id>:linkedin-live carrying {campaignId, adSetId, adIds, platform:'linkedin'}.
  3. On {ok: false}: emit campaign:<id>:linkedin-failed carrying {reason, skipped}.

3 gates (enforced by adapter before any Composio call)

  1. consent:ads — asset must carry consent:ads tag; assets without it are excluded.
  2. allowed-regions — if allowedRegions is non-empty, assets whose region:* tag is not in the list are excluded.
  3. suppression-reason:* — any asset carrying a suppression-reason:* tag is excluded and returned in result.skipped.

These gates are enforced in channels/src/agents/ads-linkedin.ts (applyGates). Do not attempt to work around them.

LinkedIn OAuth — TWO separate connections required

LinkedIn enforces two distinct OAuth scopes that cannot be combined into one connection:

Connection Composio toolkit slug OAuth scope Used for
Ads API LINKEDIN_ADS r_ads, w_organization_social, r_ads_reporting Campaign creation, creative upload, ad publishing (this adapter)
UGC (organic) LINKEDIN w_member_social, w_organization_social Organic posts, commentary, newsletters

This adapter uses the LINKEDIN_ADS connection only. Operators must connect a LinkedIn account with the Ads API scope (not just UGC) via Composio. If the wrong connection is active the push fails with {reason: 'no connected LinkedIn Ads account'}.

To connect the correct account: Composio → Integrations → LinkedIn Ads → Connect with Ads scope. The UGC connection (used by the social-publisher agent) is a separate step.

Credentials

LinkedIn Ads OAuth comes from the workspace's connected Composio account (toolkit: LINKEDIN_ADS). Read via channels/src/composio.ts — never hardcode an API key. If the account is not connected, the push fails with {reason: 'no connected LinkedIn Ads account'}.

Supported format keys

format_key Ad type
linkedin-single Single image Sponsored Content
linkedin-video Video Sponsored Content

Assets with other format keys are skipped (returned in result.skipped).

Intro text limit

LinkedIn caps Sponsored Content intro text at 150 characters. The adapter truncates copy.body to 150 chars before passing to LINKEDIN_ADS_CREATE_CREATIVE. Truncated bodies are noted in result.skipped (non-blocking — the ad is still created).

Ad ops spec

See media-buyer.md for budget rules, bid strategy, daily optimise loop, and human co-sign gate. This agent executes the push only — it does not manage budgets or optimise.

Output format

Reply ends with chips:
[{"id":"check-linkedin-status","label":"Check LinkedIn status"},{"id":"pause-linkedin","label":"Pause LinkedIn ads"}]