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 sentiment only from the source's analysis, never a guess.
  • Keep summary to one sentence that reflects only the posts shown.
  • Write one variant per network, within its maxChars where possible. Use thread for longer stories only where supportsThreads is true.
  • Give every image real alt text. Use an empty string when you can't describe it, so the user is asked.
  • Set scheduleAt only when the user asked for a time. bestTimes must come from an analytics tool.
  • Never publish until the result is approve, then publish exactly the returned variants at the returned time. On revise, rewrite from the feedback and show a new draft. On discard, 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 maxImages is flagged.
  • When to post: Post now or Schedule, with a date and time field in the draft's timeZone, and bestTimes as chips ("LinkedIn · Tue 09:00"; the reason is 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

app/api/chat/route.ts
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,
  }),
}
components/message.tsx
<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

PropTypeNotes
mode"posts" | "draft"
titlestring?Defaults to "Posts" or "Draft post"
networksNetwork[]?The networks posts and variants refer to
account{ name, handle }?Data-bound. The account a draft posts from
postsFoundPost[]?Data-bound. Posts mode, up to 30, all shown
summarystring?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
approvalRequiredboolean?The account's workflow has a teammate sign off before posts go out. Noted in the footer

Network

FieldTypeNotes
id / namestringname is shown as text, e.g. "LinkedIn"
kindstring?short, professional, photo, video, community or other. Picks the icon
maxCharsnumber?Limit per post
linkLengthnumber?Each URL counts as this many characters
supportsThreadsboolean?Enables follow-up posts and Split into thread
maxImagesnumber?Most images per post

FoundPost

FieldTypeNotes
idstringThe source's post id
networkstringA network id
author{ name, handle, verified? }
textstringVerbatim, plain text
postedAtstringISO 8601 with offset
media{ src, alt }[]?Up to 4. http(s) or data:image only
stats{ likes?, replies?, reposts?, views? }?
urlstring?http(s) link to the post
sentiment"positive" | "neutral" | "negative"?From the source's analysis

Variant

FieldTypeNotes
networkIdstringA network id
textstringThe post. Editable
threadstring[]?Follow-up posts
media{ src, alt }[]?alt: "" asks the user for alt text
hashtagsstring[]?Added after the text unless already in it
linkstring?http(s). Added after the text unless already in it

Host props: status (streaming, ready, submitting, approved, revised, discarded), onResult, result and className.

Source

On this page