[object Object]

← back to Marketing Command Center

channels: /publish skips non-postable + media-mismatched channels

29d1ff2b04b3cfc51701cd634e5e6ecda1bcb492 · 2026-07-22 07:50:51 -0700 · Steve Abrams

A full-fan-out publish used to FIRE every connected channel and collect
guaranteed errors (FB App-Review #200, LinkedIn 401, TikTok/YouTube image-as-
video). Now /publish consults the postable signal + media-type fit and SKIPS
(stages with a clear reason) any channel that can't actually take this post,
firing only the ones that will land. Result rows carry skipped:true + reason.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

Files touched

Diff

commit 29d1ff2b04b3cfc51701cd634e5e6ecda1bcb492
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Wed Jul 22 07:50:51 2026 -0700

    channels: /publish skips non-postable + media-mismatched channels
    
    A full-fan-out publish used to FIRE every connected channel and collect
    guaranteed errors (FB App-Review #200, LinkedIn 401, TikTok/YouTube image-as-
    video). Now /publish consults the postable signal + media-type fit and SKIPS
    (stages with a clear reason) any channel that can't actually take this post,
    firing only the ones that will land. Result rows carry skipped:true + reason.
    
    Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---
 modules/channels/index.js | 25 +++++++++++++++++++++----
 1 file changed, 21 insertions(+), 4 deletions(-)

diff --git a/modules/channels/index.js b/modules/channels/index.js
index 0b1dc63..097c969 100644
--- a/modules/channels/index.js
+++ b/modules/channels/index.js
@@ -870,19 +870,36 @@ module.exports = {
         }
         const st = platformStatus();
         const live = d.confirm === true && d.dryRun === false;
+        const isVideo = !!content.videoUrl;
+        const isImage = !!content.mediaUrl && !content.videoUrl;
+        // Why a channel can't actually post right now (null = clear to fire).
+        // Respects the honest `postable` signal + media-type fit so a full fan-out
+        // silently SKIPS doomed attempts — Facebook (App Review), LinkedIn (dead
+        // token), and video-only TikTok/YouTube given an image — instead of firing
+        // them and collecting guaranteed errors in the outbox.
+        const blockedReason = (ch) => {
+          const s = st[ch] || {};
+          if (!s.connected) return 'not connected';
+          if (!s.postable) return s.postableNote || 'not postable';
+          const mt = s.mediaTypes || ['image', 'text'];
+          if (isVideo && !mt.includes('video')) return 'accepts image/text only — no video';
+          if (isImage && !mt.includes('image')) return `requires video — image not accepted (accepts: ${mt.join('/')})`;
+          return null;
+        };
         const outbox = readOutbox();
         const results = [];
         for (const ch of channels) {
-          const connected = st[ch].connected;
-          if (live && connected) {
+          const block = blockedReason(ch);
+          if (live && !block) {
             const r = await ADAPTERS[ch](content, { pages: selectedPages }).catch(e => [{ ok: false, error: e.message }]);
             const ok = r.length > 0 && r.every(x => x.ok);
             outbox.push({ id: 'pub-' + ch + '-' + outbox.length, channel: ch, caption: content.caption, mediaUrl: content.mediaUrl, pages: selectedPages.length || undefined, status: ok ? 'posted' : 'failed', detail: r, at: new Date().toISOString() });
             results.push({ channel: ch, live: true, ok, detail: r });
           } else {
-            const reason = !connected ? 'pending-connection' : (d.dryRun !== false ? 'staged (dry-run)' : 'staged (needs confirm)');
+            const reason = block ? `skipped — ${block}`
+              : (d.dryRun !== false ? 'staged (dry-run)' : 'staged (needs confirm)');
             outbox.push({ id: 'stg-' + ch + '-' + outbox.length, channel: ch, caption: content.caption, mediaUrl: content.mediaUrl, status: reason, at: new Date().toISOString() });
-            results.push({ channel: ch, live: false, staged: true, reason });
+            results.push({ channel: ch, live: false, skipped: !!block, staged: true, reason });
           }
         }
         writeOutbox(outbox);

← 69af9fe channels: add honest 'postable' status — connected != can-ac  ·  back to Marketing Command Center  ·  chore: v1.7.0 (session close — Threads un-gate, image+SKU ge 0eff96e →