Skip to content

13 — Post lifecycle state machine

A "post" in Someli has a lifecycle that touches AI generation, scheduling, approval, and publishing across multiple platforms. The frontend renders the same post differently depending on its place in that lifecycle. This doc is the reference for the states and the transitions.

The authoritative encoding lives in helpers/helper.js:

  • renderPostStatus(pData) — produces the human-readable label shown next to a post.
  • calStatus(item) — produces the calendar status code used to colour-code calendar entries.

Every place in the UI that displays a post status should route through one of these two helpers. Don't reimplement the logic in a component.

States

The backend reports a post status as a string (pData.msp[0].status) plus per-channel sub-statuses (pData.msp[0].channeldata[].status). The combined view is what the frontend cares about.

State Backend status Backend approved channeldata mix Label (renderPostStatus) Calendar code (calStatus)
Generating (anything else) Generating... (falls to default)
Branding BRANDING Branding... BRANDING
Inserted INSERTED (falls through to default) INSERTED
Pending PENDING (falls through to default) PENDING
Needs approval SCHEDULED 0 Needs approval SCHEDULED
Scheduled SCHEDULED 1 Scheduled SCHEDULED
Posted (clean) POSTED no FAILED channels Published POSTED
Posted with error POSTED some FAILED channels Partially Published POSTED_WITH_ERROR
Failed (with partial) FAILED some POSTED channels Partially Published POSTED_WITH_ERROR
Failed (clean) FAILED no POSTED channels Not Published FAILED

Two important quirks that bit the team in the past:

  1. Partial publish has two backend representations. A post can end up "partially published" if either:
  2. status === 'POSTED' and at least one channel has FAILED, or
  3. status === 'FAILED' and at least one channel has POSTED. Both routes are folded into the same UI label and calendar colour. Don't try to detect partial publish by looking at only one of these — use the helpers.

  4. approved is a numeric flag, not a boolean. A post in SCHEDULED state is "Needs approval" until approved === 1. The flag is per-post; there is no per-channel approval.

Transition diagram

                  ┌──────────────┐
                  │ AI generates │
                  │  raw content │
                  └──────┬───────┘
                  ┌──────────────┐
                  │  BRANDING    │  ← logo overlay, brand colours applied
                  │              │     by AI / templates
                  └──────┬───────┘
                  ┌──────────────┐
                  │  INSERTED    │  ← saved into the queue,
                  │              │     awaiting scheduling
                  └──────┬───────┘
                         │   (user schedules a date)
                  ┌──────────────────┐
                  │  SCHEDULED       │
                  │  approved = 0    │  ← "Needs approval"
                  └──────┬───────────┘
                         │   (owner / approver clicks Approve)
                  ┌──────────────────┐
                  │  SCHEDULED       │
                  │  approved = 1    │  ← "Scheduled" — ready to publish
                  └──────┬───────────┘
                         │   (publish time arrives → backend posts to each channel)
        ┌────────────────┼────────────────┐
        ▼                ▼                ▼
   ┌─────────┐     ┌──────────────┐  ┌──────────┐
   │ POSTED  │     │ POSTED with  │  │  FAILED  │
   │ (clean) │     │ failed       │  │  (clean) │
   │         │     │ channels     │  │          │
   └─────────┘     └──────────────┘  └──────────┘
                          ↑                ↑
                          │                │
                          │   (or: FAILED with posted channels)
                          │                │
                          └────────────────┘
                          "Partially Published"
                          (POSTED_WITH_ERROR)

(PENDING is a backend-internal transient state; it is rare to see in
the UI but is mapped through to the calendar so it doesn't render as
"FAILED" by default.)

How the calendar uses these codes

pages/_accid/contentplanner/index.vue (and related calendar components) call calStatus(item) and key event styling on the result:

  • BRANDING — usually a "loading"/spinner indicator.
  • INSERTED — neutral grey (queued but not scheduled).
  • SCHEDULED — primary colour (waiting to publish).
  • POSTED — green (success).
  • POSTED_WITH_ERROR — amber (partial).
  • FAILED — red.
  • PENDING — neutral.

The exact colour mapping lives in the calendar component, not in helper.js. If you change calStatus, also check what the consuming component does with the new code.

How list views use the labels

renderPostStatus(pData) is called from list/grid views (e.g. ContentItem.vue) to produce a string the user sees. Because the helper falls through to 'Generating...' as a default, any state your component sees that isn't explicitly mapped above will render as "Generating..." — including states the backend may add in the future.

If you encounter a post stuck on "Generating..." in production:

  1. Check the actual status in the network response. It may be a value the helper doesn't recognise.
  2. Add a branch to renderPostStatus rather than special-casing in the component.

Approval

Approvals operate at the post level, not the channel level. The relevant entry points:

  • /auth/approveSkeletonPost — approve a skeleton post (raw AI output before branding).
  • /auth/approveBrandPositioning — approve the brand-positioning copy.
  • /auth/approveObjective — approve the marketing objective.
  • /auth/approveChapterData — approve chapter (BNI) data.

These are AI-generated artifacts that a human owner must sign off on before they propagate. The approval modal (components/approvalModal.vue) is the canonical UI surface; reuse it rather than building a new one.

approved === 1 on a SCHEDULED post means it will publish at the scheduled time. approved === 0 means it sits indefinitely with the "Needs approval" label.

Per-channel publish details

A post's channeldata is an array of objects, one per platform the post was scheduled to publish on. Each entry carries:

  • A provider ID (see 08-integrations.md → PROVIDER).
  • A status (POSTED / FAILED / etc.).
  • Provider-specific metadata (returned platform post ID, error message, etc.).

When debugging a "partially published" post, inspect channeldata directly — the per-platform error is there.

Reschedule and edit transitions

  • /auth/rescheduleArticle moves a SCHEDULED post to a new date. Status stays SCHEDULED; only the date changes.
  • /auth/userScheduleArticle, /auth/userScheduleCarousal, /auth/userScheduleAccountNews — schedule new posts of various types (sets status = 'SCHEDULED').
  • /auth/scheduleEventposts — schedule a holiday/event-tied post.
  • /auth/deletecuspost — hard-delete a custom post.

Editing the content of a post does not automatically reset its status. Editing a POSTED post is conceptually unusual — it changes the local record but doesn't republish. Verify with the team what the intended UX is for editing already-published content.

Reels

Reels (short-form video posts) have their own surface (pages/_accid/templateeditor/, ReelIdeasModal.vue). Their lifecycle states overlap with regular posts but they have additional generation steps (script → storyboard → video). Verify with the team for the reel-specific extensions to this state machine.

Sharp edges

  • status casing is inconsistent. helper.js uppercases (item.status?.toUpperCase()) before comparing in calStatus, but renderPostStatus does an exact-case comparison on the SCHEDULED/POSTED branches and only uppercases on the FAILED/BRANDING ones. Don't write new code that compares status directly — go through the helpers.
  • msp[0]. Both helpers reach into pData.msp[0]. The msp array is the post's "main scheduled post" record per account. If msp is empty or undefined you'll throw. Defensive callers guard with optional-chaining; do the same.
  • POSTED_WITH_ERROR is frontend-only. The backend never returns POSTED_WITH_ERROR directly; it's synthesised by calStatus. Don't try to filter posts by this status in a backend query.
  • The "Generating..." fallback hides bugs. If you add new statuses to the backend, add them to the helpers in the same PR — otherwise everything new falls into the catch-all and looks broken.