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
- Call
push_linkedin_adswith theAdPushRequestfrom the signal payload. - On
{ok: true}: emitcampaign:<id>:linkedin-livecarrying{campaignId, adSetId, adIds, platform:'linkedin'}. - On
{ok: false}: emitcampaign:<id>:linkedin-failedcarrying{reason, skipped}.
3 gates (enforced by adapter before any Composio call)
- consent:ads — asset must carry
consent:adstag; assets without it are excluded. - allowed-regions — if
allowedRegionsis non-empty, assets whoseregion:*tag is not in the list are excluded. - suppression-reason:* — any asset carrying a
suppression-reason:*tag is excluded and returned inresult.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: