Extensibility
How to publish content from Agility to social channels. Four paths built on the publish webhook: no-code automation, the Buffer API, a custom endpoint, and a custom Agility App.
Agility doesn't have a built-in "post to social" button, and that's by design. As a headless CMS, Agility owns your content and the publish event; the social networks own posting, authentication, and formatting. Connecting the two is an integration pattern, not a single setting. The good news: every approach hangs off one thing you already have, the publish webhook, so once you understand the trigger you can pick whichever path fits your team.
This guide covers four ways to do it, from a no-code recipe you can wire up in an afternoon to a fully custom endpoint, plus the caveats that trip people up.
When an editor publishes a content item, Agility fires a Content Publish Event webhook to any endpoint you've registered under Settings → Webhooks. That single HTTP POST is the starting gun for social posting. Whatever you build downstream, this is the event that kicks it off.
Before you build, two things are worth knowing about the webhook itself, because they shape every approach below:
contentID, referenceName, state), not the complete content. If your social post needs the title, body excerpt, hero image, or canonical URL, you fetch those separately via the Content Delivery or Content Management API.See the Webhooks reference for the full payload shape and setup steps.
For most teams this is the right answer. Point the webhook at an automation platform and let it handle the catch, the branching, the formatting, and the call to each social network. No servers to run, and the platform's built-in queue covers the fire-and-forget gap.
Well-documented platforms that work cleanly with an Agility webhook:
The setup is the same shape on all four:
state is Published and referenceName is your blog container.A practical refinement: rather than acting on every publish, add a Channels field to your content model (see The Channels selector pattern below) so editors decide per item where it should go, and your filter reads that field.
If your team already schedules social posts through a management platform, you can post into that instead of to each network directly. The platform then owns the OAuth tokens, per-channel formatting, scheduling, and retries, and your marketing team keeps one familiar calendar UI.
Buffer is the one we recommend building against here, because it has a public, documented GraphQL API with OAuth2 that covers X, LinkedIn, Facebook, Instagram, Threads, TikTok, Pinterest, Mastodon, Bluesky, and more. It supports exact-time scheduling via dueAt, threads, and first-comments, all things you'd otherwise hand-roll against each network.
The flow is the same as Path 1 or Path 3, except the final step calls Buffer's createPost rather than each social network. You can drive it from a no-code tool or from your own endpoint.
Trade-off worth naming. A management platform is a second subscription and a second integration to maintain, and you inherit its rate limits and uptime. It shines when a team already lives in that tool. If you just want "publish in Agility, post goes out," a direct automation recipe (Path 1) is lighter.
When you need full control over routing, formatting, scheduling logic, or token handling, point the webhook at your own serverless function. This is more work, but nothing is hidden from you.
The pattern that holds up in production:
200 immediately, then do the real work asynchronously (a queue, a background job). Because the webhook does not retry, a slow handler risks dropping the event.contentID in the payload.A minimal Next.js route handler for the receiving end:
// app/api/social/route.js
export async function POST(req) {
const event = await req.json();
// 1. Only act on publishes; acknowledge everything else immediately.
if (event.state !== "Published") {
return new Response("ignored", { status: 200 });
}
// 2. Hand off to a queue/background job so we can return 200 right away.
// (Webhooks are fire-and-forget — never block this response on the post.)
await enqueueSocialPost({
contentID: event.contentID,
referenceName: event.referenceName,
});
return new Response("queued", { status: 200 });
}
Your background worker then fetches the item and posts it:
async function processSocialPost({ contentID }) {
// Fetch the full content item (title, body, hero, canonical URL, channels).
const item = await getContentItem(contentID);
// Respect the editor's per-item channel choices.
const channels = item.fields.channels?.split(",") ?? [];
// Send to Buffer (handles OAuth, formatting, scheduling, retries).
await fetch("https://graph.buffer.com/", {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${process.env.BUFFER_TOKEN}`,
},
body: JSON.stringify({
query: CREATE_POST_MUTATION,
variables: {
channels,
text: buildPostText(item), // trims to each channel's limit
media: item.fields.heroImage?.url,
},
}),
});
}
Secure the endpoint by validating the security key you set when creating the webhook (sent as a header), so only Agility can trigger it.
If you want the posting experience to live inside the CMS, where editors write the social copy, pick channels, or schedule from a panel next to the content, build a Custom App. This is the heaviest lift and only worth it when the in-CMS editor experience is the point. Most teams get what they need from Paths 1 to 3.
Across every path, the cleanest way to control where content goes is a field on the content item itself. Add a multi-select or checkbox Channels field to your content model (for example: Website, LinkedIn, X, Newsletter). Editors choose per item, and your downstream automation or code reads that field to decide what to post and where. It keeps routing decisions with the people creating the content, and out of brittle hard-coded rules.
Agility emits a clean event, but the hard parts live on the social side and are worth setting expectations around:
| Concern | Why it matters | Where it's handled |
|---|---|---|
| No webhook retries | A missed event means a missed post | Use a tool/queue with its own retry (Paths 1, 2) |
| OAuth token expiry | Tokens for each network expire and must refresh | A platform like Buffer manages this; rolling your own means handling refresh |
| Per-channel limits | Character counts, image specs, and media rules differ | Format step (every path) |
| Rate limits & approvals | Networks throttle and some require app review | Plan for backoff; budget time for platform review |
| Publish timing | The webhook fires on Agility publish, which may not be your ideal post time | Schedule downstream (e.g. Buffer's dueAt) |
| If you want… | Use |
|---|---|
| The fastest path, no servers | Path 1 — no-code automation |
| One scheduling UI your team already uses | Path 2 — Buffer API |
| Full control over routing and formatting | Path 3 — custom endpoint + Management API |
| Editors to post from inside the CMS | Path 4 — custom Agility App |
Most teams start with Path 1 and a Channels field, then graduate to Path 2 or 3 as their needs grow. Whatever you choose, it all begins with that one publish webhook.