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 the typed manual-completion workflow that runs when a specific post was taken out of auto publish or routed to manual completion for a stated reason, and it never marks a platform that Cenelira cannot publish to.
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.
It is detected in src/lib/schedules/handoffSemantics.ts. isHandoffByModeOrStatus returns true when the schedule's publishMode or its status is HANDOFF, so the same check runs everywhere a choice between auto publish and manual completion is made.
Every supported platform has an auto-publish path, and 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 the post is a LinkedIn post with several images, which currently goes through manual completion. None of these is a limit on a whole platform.
Who this is for
Handoff packs help most where some posts have to be completed by a person in the platform's own composer, and the operator still needs a record of the assets, the reason and the completion. They help less when every post can publish automatically from start to finish.
When handoff fires
Publishing, the recovery page, the replay endpoint and the proof export all read the same check. Handoff is a state the schedule can be in, and it has no separate queue.
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 check runs everywhere, a row in handoff state appears 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 shows, so this page and the app always use the same words.
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, and the mark-done endpoint writes a typed completion event. Both are scoped to the workspace, and both refuse on the same checks 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 available for app-owned storage-backed media bundles. bundleDownloadSupported requires mediaBundleId, the FEATURE_HANDOFF_ZIP flag, supported media types, safe storage keys, and configured storage authority. External URL-only bundle assets fail closed with a typed unavailable reason instead of being proxied.
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 record
publishOutcomeForRow in src/lib/exports/workspaceProofExport.ts resolves a typed outcome for every schedule. Handoff lands as one of two values, and the version fingerprint is read from the completion event, so later edits do not change the proof entry.
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 finalised before the schema had handoffCompletedAt.
handoff_pending is set when isHandoffByModeOrStatus is true and no completion has been recorded. Replay refuses these rows, and the next action is to open the pack and finish the post on the 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 in publishing, and this page says 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 on the platform, and the app records that the completion happened.FAQ
Whenever the schedule's publishMode or its status is HANDOFF. The check is one function, isHandoffByModeOrStatus in src/lib/schedules/handoffSemantics.ts, and seven typed reason codes explain why the schedule landed there. user_selected_handoff and schedule_create_handoff are choices an operator made, platform_forced_handoff, forced_handoff_global and forced_handoff_platform are temporary forced states, linkedin_handoff_multi covers LinkedIn posts with several images, and handoff_reopened covers a workflow that was reopened after completion.
It has two, pending and completed, as defined by the HandoffWorkflowState union in src/lib/schedules/handoffPresentation.ts. The pending state shows the typed reason and the next step, and 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.
When the schedule has a media bundle, the FEATURE_HANDOFF_ZIP flag is on, and every bundle asset is app-owned storage-backed media that the server can package. That branch is bundleDownloadSupported in src/lib/schedules/handoffPackOverlay.ts. External URL-only bundle assets fail closed with a typed unavailable reason instead of being proxied.
Through POST /api/schedules/handoff with the 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 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, as PUBLIC_PLATFORM_TRUTH in src/lib/seo/platformTruth.ts records with hasAutoPublishPath: true for all eight platforms. Handoff is the typed manual-completion path for specific cases, and it never marks a platform Cenelira cannot publish to.
Last reviewed 2026-04-25