<!--
Single Endpoint to Orchestrator Reroute
Part of the Bles Software agent skills repository:
https://www.bles-software.com/claude-skills

Install for Claude Code:
  mkdir -p ~/.claude/skills/single-endpoint-to-orchestrator-reroute
  mv single-endpoint-to-orchestrator-reroute.md ~/.claude/skills/single-endpoint-to-orchestrator-reroute/SKILL.md

Install for Codex:
  mkdir -p ~/.codex/skills/single-endpoint-to-orchestrator-reroute
  mv single-endpoint-to-orchestrator-reroute.md ~/.codex/skills/single-endpoint-to-orchestrator-reroute/SKILL.md

For Gemini CLI and other agents, drop the body of this file into the
project instruction file the agent already reads.
-->

---
name: single-endpoint-to-orchestrator-reroute
description: When a feature-specific API endpoint produces a partial output (e.g. just a video, just an image) but should integrate with the platform's existing orchestration (post copy generation, history tracking, draft management, approval flow), reroute it through the orchestrator with a forceVariantId override instead of duplicating the orchestration logic. Avoids the "parallel pipeline" anti-pattern.
---

# Reroute Single-Endpoint Jobs Through the Existing Orchestrator

## When to use this skill

You added a new feature with its own endpoint (e.g. `/api/reels/preview`, `/api/podcast/generate`, `/api/3d-model/render`) that:
- Produces ONE artifact (a video, an image, an audio file, a 3D model)
- Bypasses the existing platform orchestrator (the function that normally generates post copy, saves drafts, writes to history, and prepares everything for publishing)

The artifact is great, but the user can't actually use it because:
- There's no post copy to caption it
- It doesn't appear in the dashboard's "recent generations" view
- There's no preview→confirm flow
- It doesn't get scheduled/published like everything else

**The fix is almost always a 30-line endpoint rewrite, not duplicating hundreds of lines of orchestration logic.**

## The pattern

### Anti-pattern (what NOT to do)

```javascript
// /api/reels/preview — bypasses orchestrator
app.post('/api/reels/preview', requireAuth, async (req, res) => {
  const jobId = await reelsService.startReelsJob({
    userId, variant, postContent, businessContext,
  });
  res.json({ ok: true, jobId });
});
```

This produces an MP4. But the user has nothing to do with it — no post text, no dashboard entry, no publishing path. To "fix" this naively, you'd duplicate post generation + draft saving + history saving inside `reelsService` — hundreds of lines of duplicated logic that drift over time.

### Correct pattern (route through orchestrator)

```javascript
app.post('/api/reels/preview', requireAuth, async (req, res) => {
  const { variantId, postContent } = req.body;
  // Validation: ensure the variant matches this feature's type
  const settings = await db.getEffectiveSettings(userId);
  const variant = settings.imageInstructionVariants.find(v => v.id === variantId);
  if (!variant || variant.type !== 'reels') return res.status(400).json({ error: 'wrong type' });

  // Save shell so the UI can poll immediately
  const previewId = `preview-${randomUUID()}`;
  await db.savePreviewDraft({ id: previewId, userId, status: 'pending', variantId });
  res.json({ ok: true, previewId });

  // Async: delegate to the orchestrator with forceVariantId
  setImmediate(async () => {
    try {
      await scheduleGeneratedPost({
        topic: '',
        mode: variant.postMode === 'hot_news' ? 'hot_news' : 'inspirational',
        userId,
        previewOnly: true,
        previewId,
        forceVariantId: variantId,             // ← key: forces this variant
        postInstructionsOverride: postContent, // ← caller's input becomes guidance
      });
    } catch (err) {
      await db.updatePreviewDraft(previewId, { status: 'failed', error: err.message });
    }
  });
});
```

The orchestrator (`scheduleGeneratedPost`) does:
1. Pick the variant (because forceVariantId)
2. Generate post copy via the standard `reviewEngine` for each enabled platform
3. Hit the feature-specific branch (`isReels`, `isVideoCreative`, etc.) which fires the heavy work
4. Assign the resulting media to `imagesByPlatform`
5. Save preview draft (visible in UI)
6. Save to Gen History
7. Return draft id

The user gets a preview with **media + post copy + everything** ready to confirm.

## The orchestrator branch (one-time setup)

For this to work, the orchestrator needs to know about your new variant type. Add a branch like:

```javascript
// In scheduleGeneratedPost, after post copy is generated, before final draft save:
const isReels = pickedVariant?.type === 'reels';
if (isReels && Object.keys(imagesByPlatform).length === 0) {
  const jobId = await reelsService.startReelsJob({
    userId, variant: pickedVariant, postContent: extractedPostContent,
    businessContext: settings, draftId: draft.id,
  });

  if (previewOnly) {
    // Poll until done; assign result to all platforms
    while (Date.now() - startTime < maxWaitMs) {
      const status = await reelsService.getReelsJobStatus(jobId, userId);
      if (status.status === 'ready') {
        const videoInfo = { url: status.mediaUrl, isReels: true, mediaType: 'video' };
        for (const plat of platforms) imagesByPlatform[plat] = videoInfo;
        break;
      }
      await sleep(5000);
    }
  }
}
```

Now the orchestrator handles your feature like every other variant type — no duplication.

## What to look for in the existing orchestrator

Before rerouting, identify these in your orchestrator:
1. **forceVariantId param** — does the orchestrator accept "use this specific variant"? If not, add it. (See pattern: `params.forceVariantId` checked in the variant picker.)
2. **previewOnly param** — separates preview generation from immediate publishing.
3. **previewId param** — lets the caller pre-allocate the draft id.
4. **postInstructionsOverride param** — lets the caller inject content guidance.
5. **The variant filter** — your new variant type must be in the "active creatives" filter, otherwise it'll be skipped.

If any of these are missing, add them once in the orchestrator and they're available to every future feature.

## Backfill the existing artifacts

If users have already used the broken endpoint, their artifacts won't appear in history. Backfill them:

```javascript
const id = sqlite.saveGenerationHistory({
  userId,
  previewId: existingJobId,            // use the old reels job id
  topic: 'descriptive label',
  mode: 'inspirational',
  status: 'preview',
  platforms: ['instagram', 'facebook', 'tiktok'],
  payloads: { instagram: { content: '' }, ... },  // empty → user fills via one-time-post
  images: [{ url: mediaUrl, mediaType: 'video', isReels: true }],
  videoUrl: mediaUrl,
  variantId, variantLabel, variantType: 'reels',
  settingsSnapshot: { backfilled: true },
});
```

The user can then use the existing one-time-post flow to attach the video, OR see the entry in their dashboard with manual editing.

## Common pitfalls

### Pitfall 1: Wrong cwd writes to a parallel DB
SQLite paths often use `process.cwd()`. If you run a Node one-liner from anywhere except the project root, it creates a NEW database file:

```bash
# WRONG: writes to /opt/autoposter/reels-engine/autoposter.sqlite
cd /opt/autoposter/reels-engine && node -e "require('./src/db-sqlite').save(...)"

# RIGHT: writes to /opt/autoposter/autoposter.sqlite (the production DB)
cd /opt/autoposter && node -e "require('./src/db-sqlite').save(...)"
```

When you see `id=2` from a save into a table that should have hundreds of rows, you're in a stray DB. Always `cd` to the project root before any Node maintenance command.

### Pitfall 2: Variant filter excludes new types
If the orchestrator's variant picker has:
```javascript
const active = variants.filter(v => v.type === 'image' || v.type === 'videoCreative' || ...);
```
And you forget to add your new type, the orchestrator silently skips it. Add the type to the filter (it's a one-line change but easy to miss).

### Pitfall 3: Async fire-and-forget swallows errors
`setImmediate(async () => { await scheduleGeneratedPost(...) })` — if it crashes, you never see it unless you wrap in try/catch + write the error back to the draft. Always:
```javascript
setImmediate(async () => {
  try { await scheduleGeneratedPost({...}); }
  catch (err) {
    console.error('[feature] crashed:', err);
    await db.updatePreviewDraft(previewId, { status: 'failed', error: err.message });
  }
});
```

### Pitfall 4: Endpoint returns before orchestrator completes
Always respond IMMEDIATELY with the previewId and let the orchestration run in the background. HTTP timeouts will kill long-running synchronous handlers. The client polls the preview endpoint instead.

## Reference implementation

`/opt/autoposter/src/index.js` — `/api/reels/preview` (post-rewire, ~30 lines, delegates everything)
`/opt/autoposter/src/scheduler.js` — `scheduleGeneratedPost` accepts `forceVariantId`, `previewId`, `postInstructionsOverride`, `previewOnly`. Has `isReels` branch in the same shape as `isVideoCreative`.

## Quick checklist before shipping a new artifact-producing feature

- [ ] Does the orchestrator accept `forceVariantId`? (One-time fix in the orchestrator if not.)
- [ ] Does the variant filter include your new type? (One-line add.)
- [ ] Does the orchestrator have a branch for your variant type that fires the heavy work? (10-30 lines, mirrors existing branches.)
- [ ] Does your new endpoint (if any) delegate to the orchestrator instead of duplicating? (~30 lines with shell+previewId+setImmediate.)
- [ ] Are existing artifacts (from the broken endpoint) backfilled into history?
- [ ] Did you `cd` to the project root before running any DB maintenance command?
- [ ] Are async errors caught and written back to the preview draft?

## Why this works

- **Single source of truth** — post copy generation, draft management, history, approval all live in one place. Bug fixes propagate automatically.
- **Feature parity by default** — the moment your variant is in the orchestrator, it gets EVERYTHING the platform supports (Gen History, draft preview, confirm-to-publish, automation cron, multi-user isolation).
- **Easy to add features** — the next variant type ("podcast", "3d-model", "interactive-quiz") just needs a 10-line branch in the orchestrator + variant config UI. No duplicated orchestration logic.
- **Backfilling is cheap** — old artifacts get a row in `generation_history` and instantly appear in the user's dashboard.
