Social Post
Posts the agent found, in a neutral feed, or a post it drafted for one or more networks, for the user to edit, schedule and approve before anything is published.
Experience · Shares patterns with Draft Options, Slide Gallery
≈ 2,423 tokens per use (tool 1,439 · guidance 387 · typical payload 597)How token costs are estimated
Example: Social Post
What are people saying about our launch?
Mentions of the Acme Flow launch
4 posts · X, LinkedIn and Instagram
Mostly positive on the automated gates. One pushed back on per-seat pricing.
When to use
- Posts mode: the user asks what people are saying, or to see specific posts: mentions, a competitor's launch post, top posts this week.
- Draft mode: the user asks the agent to write, announce, cross-post or schedule a social post. Show it for review before anything is published.
When not to use
- The agent couldn't fetch the posts. Never invent posts, authors or numbers.
- The user wants metrics over time, not posts. Use a Chart or KPI Card.
- The text isn't for a social network, like an email reply or a blog post. Use Email Thread or Draft Options.
How the agent should use it
- Pass found posts verbatim from the source: text, authors, stats and times. Set
sentimentonly from the source's analysis, never a guess. - Keep
summaryto one sentence that reflects only the posts shown. - Write one variant per network, within its
maxCharswhere possible. Usethreadfor longer stories only wheresupportsThreadsis true. - Give every image real alt text. Use an empty string when you can't describe it, so the user is asked.
- Set
scheduleAtonly when the user asked for a time.bestTimesmust come from an analytics tool. - Never publish until the result is
approve, then publish exactly the returned variants at the returned time. Onrevise, rewrite from the feedback and show a new draft. Ondiscard, publish nothing.
Networks are generic
Networks are data: { id, name, kind?, maxChars?, linkLength?, supportsThreads?, maxImages? }. Each is shown by its name with a neutral icon picked by kind (short posts, professional, photo, video, community or other). The component never draws a network's logo, colors or interface.
Characters are counted the way people see them (grapheme clusters, so an emoji with a skin tone counts once). When linkLength is set, every http(s) URL counts as that many characters, as some networks shorten links.
Reading posts
A summary, when given, leads the card as the answer. Every post is shown in full, in order: the name (with a verified check when the source says so), the network and a relative time ("3 hr. ago", computed after mount, with the full date in a tooltip). The handle is the name's tooltip and is read to screen readers. Text keeps its line breaks. http(s) URLs become links that open in a new tab; #hashtags and @mentions are emphasized but never linked, and an email address is not a mention. Nothing is rendered as HTML.
Up to four images sit in a grid with their alt text. Images load only from http(s) or data:image sources. Stats show as compact numbers ("1.2K") with the exact count for screen readers ("1,240 likes"). Sentiment is a word ("Negative"), not a color. An icon link (named "Open post by … (opens in a new tab)") opens the original.
Reviewing a draft
With several variants, a segmented control switches between networks; a network that needs changes is marked with an icon and "(needs changes)" for screen readers. Each variant is previewed from the user's account (name and network; the handle is a tooltip), with the text editable in place. Hashtags and the link are added to the text once, so what the user edits is what gets posted.
- Character counter once a post reaches 80% of its limit: "232 / 280", or "296 / 280 · Over by 16" in destructive text with a warning icon when over. Below that it's only read to screen readers. The text box is marked invalid too, so the state is never shown by color alone.
- Threads. On networks with
supportsThreads, an over-limit post offers Split into thread, which breaks it between sentences (then words) into posts that each fit. Every follow-up post has its own counter and can be removed; Add to thread appends one. - Images are listed with their alt text. Missing or placeholder alt text ("image", a file name) shows Add alt text with an input in place. Images can be removed, and a post with more than
maxImagesis flagged. - When to post: Post now or Schedule, with a date and time field in the draft's
timeZone, andbestTimesas chips ("LinkedIn · Tue 09:00"; thereasonis the chip's tooltip and part of its accessible name). Past times are flagged.
Approve & post (or Approve & schedule) stays disabled while any post is over its limit, an image needs alt text, or the time is missing or past; the footer says what's left. Request changes (the message icon) opens a feedback box. With approvalRequired, the footer notes "Team sign-off required". Discard drops the draft.
The component never publishes. It returns the result, and the host publishes or schedules through its own integration. For "post now", the host should show its own confirmation.
The helpers (character counting, thread splitting, compact numbers, relative and zoned times, the safe tokenizer and alt-text checks) are pure functions in social-post-utils.ts.
Installation
npx shadcn@latest add https://your-site.com/r/social-post.json
Usage
import { tool } from "ai"
import { socialPostTool } from "@/components/genui/social-post.schema"
const tools = {
// Draft mode: no execute(), the call is completed by the user's decision.
// Posts mode: return immediately, e.g. execute: async () => ({ shown: true }).
[socialPostTool.name]: tool({
description: socialPostTool.description,
inputSchema: socialPostTool.inputSchema,
}),
}<SocialPost
{...part.input}
status={part.state === "input-streaming" ? "streaming" : undefined}
result={part.state === "output-available" ? part.output : undefined}
onResult={(result) =>
addToolResult({ tool: "show_social_post", toolCallId: part.toolCallId, output: result })
}
/>In draft mode the agent receives one of:
{
action: "approve",
variants: [
{ networkId: "linkedin", text: "Release prep used to take…" },
{ networkId: "x", text: "Release prep shouldn't take two days…", thread: ["https://acme.example/webinar…"] },
],
when: "2026-10-13T09:00:00-04:00", // or "now"
edited: true,
}
{ action: "revise", feedback: "Lead with the date" }
{ action: "discard" }text is the full first post, hashtags and link included; thread holds the follow-ups; media (when the draft had images) holds the remaining images with their final alt text. Publish exactly that. While the host publishes or schedules, pass status="submitting".
With LangGraph
Push it from a graph node with ui.push({ name: "show_social_post", props: { toolCallId, ...args } }, { message }), and render the thread's UI messages with the Renderer.
Restoring from history
Pass the stored result back as result. An approved draft shows the final posts read-only with "Scheduled for Tue, Oct 13, 09:00 EDT" or "Sent to publish", requested changes show the feedback, and a discarded draft leaves a short note. The component never calls onResult again.
<SocialPost
{...part.input}
result={part.state === "output-available" ? part.output : undefined}
/>Schema
| Prop | Type | Notes |
|---|---|---|
mode | "posts" | "draft" | |
title | string? | Defaults to "Posts" or "Draft post" |
networks | Network[]? | The networks posts and variants refer to |
account | { name, handle }? | Data-bound. The account a draft posts from |
posts | FoundPost[]? | Data-bound. Posts mode, up to 30, all shown |
summary | string? | Posts mode. One sentence across the posts |
draft | { variants, scheduleAt?, timeZone? }? | Draft mode. 1–6 variants |
bestTimes | { networkId, at, reason? }[]? | Data-bound. From an analytics tool. reason is the chip's tooltip |
approvalRequired | boolean? | The account's workflow has a teammate sign off before posts go out. Noted in the footer |
Network
| Field | Type | Notes |
|---|---|---|
id / name | string | name is shown as text, e.g. "LinkedIn" |
kind | string? | short, professional, photo, video, community or other. Picks the icon |
maxChars | number? | Limit per post |
linkLength | number? | Each URL counts as this many characters |
supportsThreads | boolean? | Enables follow-up posts and Split into thread |
maxImages | number? | Most images per post |
FoundPost
| Field | Type | Notes |
|---|---|---|
id | string | The source's post id |
network | string | A network id |
author | { name, handle, verified? } | |
text | string | Verbatim, plain text |
postedAt | string | ISO 8601 with offset |
media | { src, alt }[]? | Up to 4. http(s) or data:image only |
stats | { likes?, replies?, reposts?, views? }? | |
url | string? | http(s) link to the post |
sentiment | "positive" | "neutral" | "negative"? | From the source's analysis |
Variant
| Field | Type | Notes |
|---|---|---|
networkId | string | A network id |
text | string | The post. Editable |
thread | string[]? | Follow-up posts |
media | { src, alt }[]? | alt: "" asks the user for alt text |
hashtags | string[]? | Added after the text unless already in it |
link | string? | http(s). Added after the text unless already in it |
Host props: status (streaming, ready, submitting, approved, revised, discarded), onResult, result and className.
