Manual completion workflow

Handoff is the typed fallback for posts that should not publish automatically

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.

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?

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.

Limit

What is handoff not?

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

Built for operators who have to finish a post on the platform and prove it later

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.

For
  • Agency operators completing LinkedIn posts with several images, which currently go through manual completion.
  • Teams who want a typed record of why a post was completed by hand, attributable to a workspace user.
  • Operations leads who need one workflow for manual posts chosen by a user, manual states forced by Cenelira and the LinkedIn case with several images, 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 published automatically with no manual fallback. Handoff happens only when a user chooses it or Cenelira forces it.
  • Operators who expect Cenelira to publish for them in another company's composer. Handoff records the completion and never acts in the platform's interface.

When handoff fires

One check on two fields decides handoff

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

Seven typed reasons, each with 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 shows, so this page and the app always use the same words.

  • 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 endpoint reads the pack and one records the completion

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

The proof record keeps the outcome and the version at completion

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

Where handoff stops

Handoff packs cover one workflow in publishing, and this page says where it stops.

  • Every supported Cenelira platform has an auto-publish path, and handoff is the typed fallback for specific cases rather than a sign that a platform cannot be published to. 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 on the platform, and the app records that the completion happened.
  • Bundle ZIP download is supported for app-owned storage-backed media bundles when the feature flag is enabled. External URL-only bundle assets are not proxied and receive a typed unavailable reason when they cannot be packaged.
  • External reviewer identity on a related shared review link is self-declared. Handoff records who completed the workflow inside the workspace and says nothing 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 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.

How many states does the workflow have?

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.

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?

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.

How does completion get recorded?

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.

How does handoff appear on the proof record?

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.

Why call this a fallback instead of a platform limitation?

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

Handoff packs | Cenelira