Posts
A post is a LinkedIn post draft moving through lk-wiz's content pipeline:
draft → in_review → revision_requested → approved → scheduled → published → archived. Posts are where writing actually happens — the canvas editor,
real-time collaboration, version history, and image attachments all hang off
a post record.
This page describes the product-facing behavior. For the exact request/response
shapes and error codes, see the Posts API reference. For
programmatic access from an AI client, see Posts MCP tools. The
underlying spec this page is based on is specs/features/posts.md in the repo.
This page covers the five post capabilities end to end.
Create
You get a post in one of two ways:
- From scratch — click "New Post" on the pipeline board, give it a title, start typing.
- From an idea — click "Generate Post" on a triaged idea; GenAI drafts
the content, and on save the new post is linked back to the idea
(
source_idea_id) and the idea flips toconverted.
Either way, the post starts life as a draft at p3_medium priority,
version 1, with an empty (or GenAI-seeded) canvas that's immediately
collaborative — open it in two browser tabs and you'll see both cursors.
POST /api/posts{
"workspace_id": "ws_123",
"title": "Why observability matters",
"content": "<p>Optional starting content</p>",
"source_idea_id": "idea_456",
"tags": ["engineering"]
}Why is there a yjs_state field?
When the frontend creates a post from GenAI-generated content, it encodes
the initial editor state as a raw Yjs update
(Y.encodeStateAsUpdate(doc)) and base64s it into yjs_state. The backend
wraps that as a y-protocol syncStep2 message so the very first WebSocket
client to open the post receives the generated draft, not a blank document.
If you don't pass it (or pass something unparseable), the post just starts
with an empty collaborative document — creation never fails because of it.
Manage
The pipeline board is a kanban view of a workspace's posts, one column
per status. Cards show title, author, priority badge, and character count;
drag a card between columns to change its status (respecting the transition
rules below), or drag within a column to reorder (persisted via
POST /api/posts/reorder).
Status transitions
Two transitions are special: draft → in_review and in_review → approved
each automatically snapshot a version of the content before the status
changes, so you can always see exactly what was submitted or approved (see
Versions below). Requesting a revision requires a comment
explaining what needs to change; scheduling requires a target date/time.
Any workspace member can drive a status transition through
PUT /api/posts/{id}/status — the only enforcement is the state-machine
table itself. Role restrictions kick in specifically for the dedicated
schedule and publish actions, and for deleting a
non-draft/archived post (admin/owner only).
Editing & deleting
Editing title, content, tags, priority, reviewer, or LinkedIn @-tags
(tagged_entities) is a partial update — send only the fields you're
changing. Deleting is restricted by post status, not by ownership: any
workspace member can delete a draft or archived post (regardless of who
authored it), but only admins/owners can delete a post mid-pipeline
(in_review, approved, scheduled, published) — there's no separate
"only the author can delete their own draft" check.
Publish
Once a post is approved, it's ready to leave the drafting stage. lk-wiz
doesn't publish to LinkedIn directly — actual publishing happens in Zoho
Social, which owns the LinkedIn API integration. lk-wiz's job is to manage
the content pipeline and the weekly publishing calendar up to that point.
Two ways an approved post moves forward:
- Schedule it —
POST /api/posts/{id}/schedulewith ascheduled_atslot. This can be a slot the system auto-assigned (a daily background job setsscheduled_atto roughly one day out for any approved post that doesn't have one yet — it isn't a weekly-calendar lookup) or a custom date/time you pick yourself. - Publish it immediately —
POST /api/posts/{id}/publish, once you've manually copied the content into Zoho Social and it's live there.
Both actions are gated to the post's author, an editor, or an
owner — a workspace viewer cannot schedule or publish.
What happens around a scheduled slot, handled by a separate background job, not this API:
- 2 days before the slot: the job logs a reminder and emits a
SlackNotificationSentmetric — it does not actually send a Slack DM to the author today. - 24 hours after a slot passes with the post still
scheduled: the job logs a warning and emits aMissedSchedulemetric — again, no Slack message is sent. (The Slack-sending code for these events exists elsewhere in the codebase, but the scheduler job doesn't call it.)
Versions
Every meaningful change to a post's content is recoverable. Open Version History from the editor toolbar to see a chronological list of snapshots, each tagged with who made it, when, and why:
| Trigger | Fires when |
|---|---|
manual | You click "Save Version" in the editor |
submit_review | The post moves draft → in_review |
approved | The post moves in_review → approved |
genai_accept | You accept a GenAI rewrite suggestion (recorded by the GenAI domain, same version shape) |
Click any version to diff it against the current content; "Restore" first snapshots the current content (so restoring never destroys work) and then replaces the editor content with the selected version.
GET /api/posts/{id}/versions?workspaceId=ws_123Reading version history requires more than plain membership — workspace
viewers cannot view version history (403). Creating a manual version
only requires membership.
Images
Attach images to a post (charts, screenshots, graphics) for use when copying the post to Zoho Social.
- Ask for an upload slot:
POST /api/posts/{id}/images { "filename": "chart.png", "content_type": "image/png" }→ returns a presigned S3upload_urlvalid for one hour. Allowed types: PNG, JPG/JPEG, GIF, WEBP. PUTthe raw file bytes straight toupload_urlfrom the browser — the file never transits through Lambda.GET /api/posts/{id}/imageslists everything attached, each with a freshly generated presigned download URL (also one-hour, regenerated every time you list — never cached server-side).- Delete via
DELETE /api/posts/{id}/images/{image_id}— allowed for whoever uploaded it, the post's author, or an admin/owner.
See also
- Posts API reference — full endpoint list, request/response shapes, status codes.
- Posts MCP tools — read-only
list_posts/get_post/get_pipeline_statusfor AI clients. - Ideas feature guide — the upstream source of
source_idea_id. - Real-Time Collaboration — the Yjs/WebSocket layer a post's canvas runs on.