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.
Manual completion workflow
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.
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.
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.
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
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.
When handoff fires
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
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.
Switched to manual completion
A user turned off automatic publishing for this schedule.
Created for manual completion
This schedule started in manual mode and needs a manual publish pass.
Automatic publishing is temporarily unavailable
This platform is currently in forced manual handoff mode.
Manual completion is currently forced
Automatic publishing is temporarily disabled across all platforms.
Manual completion is forced for this platform
Automatic publishing is temporarily disabled for this platform.
LinkedIn multi-image needs manual completion
LinkedIn multi-image posts currently require a manual publish pass.
Manual completion was reopened
This schedule was reopened and needs another manual publish pass.
Pack 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
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
Handoff packs cover one workflow on the publish path. This page is explicit about where it stops.
PUBLIC_PLATFORM_TRUTH records hasAutoPublishPath: true for all eight platforms.handoffPresentation.ts read "temporarily unavailable" and "temporarily disabled", and the workflow returns to auto when the forced state is lifted.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.FAQ
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.
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.
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.
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.
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.
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.
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