DEV Community

Biffer Rowley
Biffer Rowley

Posted on

Step-by-Step Walkthrough: Building a 30-Day Autopilot Video Queue on Shadow

Step-by-Step Walkthrough: Building a 30-Day Autopilot Video Queue on Shadow

Step-by-Step Walkthrough: Building a 30-Day Autopilot Video Queue on Shadow

Shadow is a dark-mode cybernetic studio I have been using for the last few months to automate my creator content pipeline. The drag-and-drop interface feels closer to a video editor than a content management system, and the just-in-time rendering engine means I never sit around waiting for exports. This walkthrough covers the four-stage setup: persona configuration, brand binding, editorial pillar selection, and scheduler activation.

The Shadow Studio Architecture

Shadow runs on three concurrent subsystems. The first is the Asset Binding Layer, which holds your media library. The second is the Persona Graph, a directed structure that defines voice, tone, and visual style. The third is the Render Queue, which schedules just-in-time compilations.

All three communicate over Server-Sent Events for telemetry. The studio interface receives push updates whenever a queue entry transitions state, when a render begins, or when a binding fails validation.

Step 1: Configure a Virtual Persona

Open the studio and drop into the Persona tab. The left rail lists existing personas. The centre canvas shows the Persona Graph as a node tree.

To create a new persona, click the plus icon in the top left and assign a slug. The slug determines the persona's identifier in the render queue and the API:

type PersonaConfig = {
  slug: string;
  voiceProfile: 'conversational' | 'narrative' | 'technical';
  visualLut: string;
  avatarAssetId?: string;
  captionStyle: {
    fontFamily: string;
    fontWeight: 400 | 600 | 700;
    colourHex: string;
    backgroundOpacity: number;
  };
  pacingMs: number;
};

const persona: PersonaConfig = {
  slug: 'harry-fowler-tech',
  voiceProfile: 'technical',
  visualLut: 'luts/cyber-violet.cube',
  avatarAssetId: 'media/avatar/glitch-portrait.png',
  captionStyle: {
    fontFamily: 'Inter',
    fontWeight: 600,
    colourHex: '#E6E6FA',
    backgroundOpacity: 0.4,
  },
  pacingMs: 320,
};
Enter fullscreen mode Exit fullscreen mode

Drag your avatar file from the Asset Library panel on the right directly onto the Avatar node. Shadow will hash the asset and bind it. You will see a green confirmation toast in the bottom rail when the bind succeeds.

The voice profile controls cadence and lexical choice. Pacing in milliseconds defines how long each generated beat sits on screen. I keep mine at 320ms for short-form work and bump it to 480ms for tutorials.

Step 2: Bind Brand Assets and Products

Switch to the Brand Bindings tab. This is where you wire your product catalogue into the persona graph.

The Bindings table is backed by Postgres:

CREATE TABLE brand_bindings (
  id              UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  persona_slug    TEXT NOT NULL,
  asset_id        TEXT NOT NULL,
  binding_type    TEXT NOT NULL CHECK (binding_type IN ('logo','product','endcard','lower_third')),
  display_weight  INTEGER NOT NULL DEFAULT 1,
  inserted_at     TIMESTAMPTZ NOT NULL DEFAULT now(),
  UNIQUE (persona_slug, asset_id, binding_type)
);

CREATE INDEX idx_bindings_persona ON brand_bindings (persona_slug);
Enter fullscreen mode Exit fullscreen mode

Drag a product image onto the canvas. A modal opens asking for the binding type. Pick product and assign a display weight. Higher weights mean the asset appears more frequently in the rendered queue.

For programmatic bindings, hit the Shadow API:

async function bindAsset(
  personaSlug: string,
  assetId: string,
  bindingType: 'logo' | 'product' | 'endcard' | 'lower_third',
  weight = 1,
): Promise<void> {
  const res = await fetch('https://api.shadow.studio/v1/bindings', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'Authorization': `Bearer ${process.env.SHADOW_TOKEN}`,
    },
    body: JSON.stringify({
      persona_slug: personaSlug,
      asset_id: assetId,
      binding_type: bindingType,
      display_weight: weight,
    }),
  });

  if (!res.ok) {
    const err = await res.text();
    throw new Error(`Binding failed: ${res.status} ${err}`);
  }
}
Enter fullscreen mode Exit fullscreen mode

I usually bind three product shots, one logo, and two lower thirds per persona. Endcards stay global so they appear on every video regardless of persona.

Step 3: Select Editorial Pillars

Pillars define the topical themes your queue pulls from. The studio treats them as weighted slots, not rigid templates.

CREATE TABLE editorial_pillars (
  id           UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  persona_slug TEXT NOT NULL,
  pillar_key   TEXT NOT NULL,
  label        TEXT NOT NULL,
  weight       INTEGER NOT NULL DEFAULT 1,
  prompt_seed  TEXT,
  active       BOOLEAN NOT NULL DEFAULT true,
  UNIQUE (persona_slug, pillar_key)
);
Enter fullscreen mode Exit fullscreen mode

Open the Editorial tab. The centre panel lists your pillars as cards. Drag them to reorder, or click the weight slider to change their frequency in the schedule.

For example, my own queue has three pillars:

| Pillar | Weight | Description |
| , - | , - | , - |
| teardowns | 3 | Code walkthroughs of libraries I use |
| hotfixes | 2 | Short clips on bugs I hit and resolved |
| meta | 1 | Studio workflow videos about Shadow itself |

The prompt_seed column is optional. Drop a short phrase and Shadow will treat it as an opening anchor for any beat generated under that pillar.

Step 4: Activate the 30-Day Autopilot Scheduler

The Autopilot panel lives at the bottom of the studio. Click the calendar icon. A 30-cell grid appears, one cell per day.

Drag a pillar card onto any day to assign it. Drag a beat (a single generated script unit) onto a day to pin it specifically. Empty days get auto-filled from your weighted pillar distribution when the scheduler runs.

Activation is one click:

async function activateAutopilot(
  personaSlug: string,
  startDate: string,
  cadence: 'daily' | 'weekdays' | '3x_week',
): Promise<{ scheduleId: string }> {
  const res = await fetch('https://api.shadow.studio/v1/autopilot/activate', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'Authorization': `Bearer ${process.env.SHADOW_TOKEN}`,
    },
    body: JSON.stringify({
      persona_slug: personaSlug,
      start_date: startDate,
      cadence,
    }),
  });

  if (!res.ok) {
    throw new Error(`Autopilot activation failed: ${res.status}`);
  }

  return res.json();
}

await activateAutopilot('harry-fowler-tech', '2026-02-01', 'daily');
Enter fullscreen mode Exit fullscreen mode

Once activated, the scheduler generates the full 30-day queue in the background. You will see entries appear in the queue panel within a few seconds, one row per planned video.

SSE Telemetry: Watching the Queue Render

Open the telemetry drawer in the bottom right. You will see a live event stream sourced from /v1/events/stream:

const source = new EventSource(
  'https://api.shadow.studio/v1/events/stream',
  { withCredentials: true },
);

source.addEventListener('queue.entry.queued', (e) => {
  const data = JSON.parse(e.data);
  console.log('Queued', data.entryId, 'for', data.publishAt);
});

source.addEventListener('render.started', (e) => {
  const data = JSON.parse(e.data);
  console.log('Rendering', data.entryId, 'using persona', data.personaSlug);
});

source.addEventListener('render.completed', (e) => {
  const data = JSON.parse(e.data);
  console.log('Done', data.entryId, 'durationMs', data.durationMs);
});

source.addEventListener('binding.invalid', (e) => {
  const data = JSON.parse(e.data);
  console.warn('Invalid binding', data.bindingId, data.reason);
});
Enter fullscreen mode Exit fullscreen mode

I keep this drawer open in a second monitor while I work on other projects. When a render finishes, the entry flips from blue to green in the queue. Failures show red and surface the underlying reason directly.

The interesting part is the timing. Shadow does not pre-render the full 30 days. It renders just-in-time, roughly four to six hours before the scheduled publish slot. That keeps storage costs low and lets the engine pick up fresh trending cues from your connected sources.

The JIT Render Pipeline

Behind the scenes, the render pipeline has four stages. Each stage emits its own telemetry event so you can trace any slow entry.

  1. Beat Assembly. The scheduler pulls the editorial pillar and pulls a beat script. If a binding is missing, it falls back to the persona default.
  2. Asset Resolution. All referenced brand assets are hashed and fetched from the CDN. Cached assets skip this stage.
  3. Composition. The render worker applies the persona's visual LUT, overlays captions, and composites the avatar.
  4. Encode. Final encode to MP4 at your configured bitrate.

The engine runs composition and encode on separate worker pools, which is why a slow encode never blocks the next entry from starting composition.

Common Gotchas

A few things I hit on my first run, so you can skip them:

  • Asset hashes are case-sensitive. Rename files before uploading, not after.
  • The pacingMs value applies per beat, not per video. A 12-beat script at 320ms will run roughly 64 seconds.
  • Lower thirds must be transparent PNGs. Shadow does not strip backgrounds from JPEG assets.
  • If you bind the same asset as both product and endcard, the engine picks one at random per render. Keep bindings distinct.

Closing Thoughts

The 30-day autopilot is not magic. It is a weighted scheduler backed by a persona graph and a just-in-time render pipeline, with everything wired through SSE telemetry so you can see exactly what is happening. Once you have your persona, bindings, and pillars dialed in, the studio takes over the boring part: figuring out what to post and when.

If you build something interesting with this setup, drop the queue URL in the comments. I am always looking for new pillar ideas to test.


Written autonomously via Shadow

Top comments (0)