Manual completion workflow

Handoff is the typed fallback when auto publish is the wrong answer.

Every supported Cenelira platform has an auto-publish path. PUBLIC_PLATFORM_TRUTH in src/lib/seo/platformTruth.ts records hasAutoPublishPath: true for all eight platforms. Handoff is not a label for "platforms we cannot publish to". It is the typed manual-completion workflow that runs when a specific row was opted out of auto publish or routed through manual for a stated reason.

The detection is one function: isHandoffByModeOrStatus in src/lib/schedules/handoffSemantics.ts returns true when the schedule's publishMode equals HANDOFF or its status equals HANDOFF. Seven typed reason codes record why the row landed there, and the workflow has two states: pending and completed.

Definition

What is a handoff pack?

A Cenelira handoff pack is the typed manual-completion workflow that runs when a schedule's publishMode or status is HANDOFF. It carries a typed reason, a single asset action, and a recorded completion event.

Mechanism

Where is handoff detected?

In src/lib/schedules/handoffSemantics.ts. isHandoffByModeOrStatus returns true when the schedule's publishMode equals HANDOFF or its status equals HANDOFF, so the same predicate runs at every gate that needs to choose between auto publish and manual completion.

Limit

What is handoff not?

Every supported platform has an auto-publish path. Handoff is the fallback for specific cases: a user chose manual mode, a schedule was created in manual mode, the platform is in forced handoff for a stated reason, Cenelira reopened the workflow, or LinkedIn multi-image currently routes through manual. It is not a platform-wide limitation.

Who this is for

Built for operators who have to finish a post in-platform and prove it later.

Handoff packs earn their rent on teams where some posts have to be completed by a human in the platform composer, and the operator still needs a typed record of the assets, the reason, and the completion. They add less value when every post can be auto published end to end.

For
  • Agency operators completing LinkedIn multi-image posts that currently route through manual.
  • Teams who want a typed record of why a post was completed by hand, attributable to a workspace user.
  • Ops leads who need a single workflow for opt-in manual posts, forced manual states, and the LinkedIn multi-image case, with the same proof export at the end.
Not for
  • Workflows that rely on screenshots, calendar events, or chat messages as the manual-completion record.
  • Teams that want every post auto published with no manual fallback path. Handoff is opt-in or forced; it is never assumed.
  • Operators who expect Cenelira to publish on their behalf in a third-party composer. Handoff records the completion; it does not act in the platform UI.

When handoff fires

One predicate, two fields, no ambiguity.

The publish path, the reliability surface, the replay endpoint, and the proof export all read the same predicate. Handoff is not a separate queue; it is a state on the schedule that the same row can be in.

isHandoffByModeOrStatus(publishMode, status) in src/lib/schedules/handoffSemantics.ts returns true if either publishMode === 'HANDOFF' or status === 'HANDOFF'. The publishMode field records the operator-chosen workflow; the status field records the current execution state. Either is sufficient.

Because the predicate runs everywhere, a row in handoff state surfaces as a typed handoff_pending reliability item, the replay endpoint refuses with handoff-not-replayable, and the proof export records handoff_pending on the schedule's publishOutcome.

The workflow has two states. HandoffWorkflowState in src/lib/schedules/handoffPresentation.ts is pending | completed. The pending state shows the typed reason and the next step. The completed state records who marked it done and when.

Reason codes

Seven typed reasons. Each has a published label.

REASON_BY_CODE in src/lib/schedules/handoffPresentation.ts maps each code to the label and detail the in-app handoff banner shows. The strings below are the same strings the app renders, so the public page and the in-app surface stay aligned without translation.

  • user_selected_handoff

    Switched to manual completion

    A user turned off automatic publishing for this schedule.

  • schedule_create_handoff

    Created for manual completion

    This schedule started in manual mode and needs a manual publish pass.

  • platform_forced_handoff

    Automatic publishing is temporarily unavailable

    This platform is currently in forced manual handoff mode.

  • forced_handoff_global

    Manual completion is currently forced

    Automatic publishing is temporarily disabled across all platforms.

  • forced_handoff_platform

    Manual completion is forced for this platform

    Automatic publishing is temporarily disabled for this platform.

  • linkedin_handoff_multi

    LinkedIn multi-image needs manual completion

    LinkedIn multi-image posts currently require a manual publish pass.

  • handoff_reopened

    Manual completion was reopened

    This schedule was reopened and needs another manual publish pass.

Pack contract

One read endpoint, one write endpoint, one typed contract.

The pack endpoint returns a typed overlay; the mark-done endpoint writes a typed completion event. Both are workspace-scoped and both refuse on the typed gates the rest of the schedule path enforces.

GET /api/handoff/pack?scheduleId= in src/app/api/handoff/pack/route.ts returns the schedule overlay used by the in-app handoff surface. The contract is built by buildHandoffPackOverlayContract in src/lib/schedules/handoffPackOverlay.ts: a typed assetActionKind (download_pack, download_media, or open_media), a typed downloadKind (bundle_zip or single_media) when a download is supported, and a typed unavailable reason when it is not.

Bundle ZIP downloads are gated to LinkedIn today. bundleDownloadSupported requires mediaBundleId, the FEATURE_HANDOFF_ZIP flag, and a LinkedIn platform. Other platforms surface a single-media download or open action when the asset is reachable, and a typed unavailable reason when the asset cannot be packaged.

POST /api/schedules/handoff in src/app/api/schedules/handoff/route.ts takes a body of {scheduleId, done}. When done is true, the route records a SCHEDULE_HANDOFF_COMPLETED collab event, attributes the completion to the workspace user, and writes a handoff completion receipt that the proof export reads later.

When done is false, the route reopens the workflow with reason code handoff_reopened. The workflow returns to the pending state and the proof outcome reverts until the next completion event lands.

Proof artifact

The proof record records the outcome and the version on completion.

publishOutcomeForRow in src/lib/exports/workspaceProofExport.ts resolves a typed outcome for every schedule. The handoff path lands as one of two values, and the version fingerprint is read from the completion event so the proof entry survives later edits.

handoff_completed is set when handoffCompletedAt is truthy, or when the post row records SUCCESS with platformPostId === null while isHandoffByModeOrStatus is true. The second branch covers schedules whose post row was finalized before the schema carried handoffCompletedAt.

handoff_pending is set when isHandoffByModeOrStatus is true and no completion has been recorded. Replay refuses these rows; the next action is to open the pack and finish the post in-platform.

versionFingerprint on a handoff_completed row is read first from the SCHEDULE_HANDOFF_COMPLETED event's reviewVersion, then from the latest REVIEW_APPROVED or EXTERNAL_REVIEW_APPROVED event, and finally from the schedule row. The completion event carries the reviewVersion the schedule held when the completion was committed, so the proof row stays bound to the version the operator actually completed against. See the version-bound approval page for the binding the rest of the review path enforces.

The same row carries exceptionCause, recoveryAction, and recoveryActor when the schedule moves through a reliability item before completion. See the failed-post recovery page for the typed cause and next-action codes that drive those columns.

What this does not claim

Honest limits on the handoff path.

Handoff packs cover one workflow on the publish path. This page is explicit about where it stops.

  • Every supported Cenelira platform has an auto-publish path. Handoff is the typed fallback for specific cases, not a label for platform incapability. PUBLIC_PLATFORM_TRUTH records hasAutoPublishPath: true for all eight platforms.
  • The forced-handoff reasons are recorded as temporary states. Their labels in handoffPresentation.ts read "temporarily unavailable" and "temporarily disabled", and the workflow returns to auto when the forced state is lifted.
  • Cenelira does not act in the third-party platform composer. composerUrl in the pack route returns a deep link for LinkedIn, Threads, X, TikTok, Pinterest, and YouTube. The operator publishes in-platform; the app records that the completion happened.
  • Bundle ZIP download is supported for LinkedIn today. Other platforms surface a single-media download or open action when the asset is reachable, and a typed unavailable reason when it cannot be packaged.
  • External reviewer identity on a related review link is self-declared, not authenticated. Handoff records who completed the workflow inside the workspace; it does not assert anything about who reviewed the content earlier.
  • This page describes how Cenelira routes specific schedules through manual completion and records the result. It makes no claim about how other publishing tools structure their own manual workflows.

FAQ

Short answers for operators.

When does Cenelira route a schedule through handoff?

Whenever the schedule's publishMode equals HANDOFF or its status equals HANDOFF. The check is one function, isHandoffByModeOrStatus in src/lib/schedules/handoffSemantics.ts. Seven typed reason codes explain why the schedule landed there; user_selected_handoff and schedule_create_handoff are operator choices, platform_forced_handoff and forced_handoff_global and forced_handoff_platform are temporary forced states, linkedin_handoff_multi covers LinkedIn multi-image, and handoff_reopened covers a workflow that was reopened after completion.

How many states does the workflow have?

Two: pending and completed. The HandoffWorkflowState union in src/lib/schedules/handoffPresentation.ts is the source. The pending state shows the typed reason and the next step; the completed state records who marked it done and when.

What does the pack endpoint return?

GET /api/handoff/pack?scheduleId= returns the schedule overlay used by the in-app handoff surface, including a typed asset action, a typed download kind when available, and a typed unavailable reason when the asset cannot be packaged. The contract is built by buildHandoffPackOverlayContract in src/lib/schedules/handoffPackOverlay.ts.

When is a bundle ZIP available?

Today, only when the schedule has a media bundle, the FEATURE_HANDOFF_ZIP flag is on, and the platform is LinkedIn. That branch is bundleDownloadSupported in src/lib/schedules/handoffPackOverlay.ts. Other platforms surface a single-media download or open action when the asset is reachable, and a typed unavailable reason when it isn't.

How does completion get recorded?

POST /api/schedules/handoff with body {scheduleId, done}. When done is true, the route records a SCHEDULE_HANDOFF_COMPLETED collab event, attributes the completion to the user, and writes a handoff completion receipt that the proof export reads later.

How does handoff appear on the proof record?

publishOutcome lands on the signed 17-field proof record as handoff_completed when handoffCompletedAt is set or when the post row records SUCCESS with no platformPostId in handoff mode, and as handoff_pending when isHandoffByModeOrStatus is true and no completion has been recorded. The precedence is encoded in publishOutcomeForRow in src/lib/exports/workspaceProofExport.ts.

Why call this a fallback instead of a platform limitation?

Because every platform Cenelira supports has an auto-publish path. PUBLIC_PLATFORM_TRUTH in src/lib/seo/platformTruth.ts records hasAutoPublishPath: true for all eight platforms. Handoff is the typed manual-completion path for specific cases, not a label for "platforms we cannot publish to."

Last reviewed 2026-04-25

Handoff packs · Cenelira – Cenelira