ข้ามไปที่เนื้อหา

Jev decision routing example

Use Jev to decide what to post then create RedPanda social scheduler drafts in Global or per-channel mode via Node SDK.

4 min read

Connect your agent today

Draft from chat, review in your calendar, and publish only what you approve.

เริ่มใช้งานฟรี

What this example does

Before you schedule anything, ask whether the post is worth publishing. Classic social guidance boils down to three checks — relevant, engaging, and actionable — plus whether copy and visuals fit each network (decide what to post on social media).

This guide encodes those checks as Jev questions, then maps the answers to RedPanda JSON:

  1. Decide — Is the post ready? Does it need media? Should it use Global mode (one caption everywhere) or per-channel overrides?
  2. Execute — @openquok/node-sdk creates a draft with the right shape (body only, or bodiesByIntegrationId / providerSettingsByIntegrationId).
  3. Approve — A human reviews on the calendar or kanban before publish.

Dashboard equivalents: Global vs per-channel in the composer. API equivalents: isGlobal, bodiesByIntegrationId, and mediaByIntegrationId on POST /public/posts.

Decision rules (what to post)

CheckQuestion for JevRedPanda follow-up
RelevantIs this useful to the audience (inform, entertain, or community)?Low score → human_review, do not create a draft
EngagingWould someone want to read and react?Very low → revise copy or discard
ActionableWould someone share, comment, or click?Optional gate for automated scheduling
VisualsDoes the post need photo or video?needs_media high → draft with media[] or kanban note to attach assets
Channel fitSame copy on every network, or customized per channel?Maps to Global vs per-channel JSON (see below)

Do not cross-post identical copy when networks need different tone, length, or format. RedPanda supports one shared caption (Global) or per-integration overrides — the same split as “customized for the channel it’s on.”

Global vs per-channel in the API

Composer modeWhen to usePublic API shape
GlobalSame announcement and attachments everywherebody, media, integrationIds[], isGlobal: true
Per-channel copyDifferent caption or media per networkbodiesByIntegrationId, optional mediaByIntegrationId
Global copy, per-channel settingsSame words; only YouTube title, Instagram post type, etc. differShared body + providerSettingsByIntegrationId per UUID

See Global vs per-channel and recipe multi-platform-campaign.json under agent/skills/openquok-core/resources/examples/.

Prerequisites

RequirementNotes
TYPESAFE_API_KEYFrom console.typesafe.ai
opo_ access tokenPer user after OAuth2 Authorization Code, or a workspace token for scripts
Channel UUIDsFrom GET /public/integrations — one or more for Global / multi-channel drafts
npm install @typesafe-ai/sdk @openquok/node-sdk

Runnable copy: sdk/examples/jev-route-draft.mjs.

TypeScript reference

Pass proposed caption, target channels, and optional per-channel variants in state. Jev returns compose mode and quality gates; your code builds PublicCreatePostDto.

import { choice, noul, score, TypeSafeClient } from "@typesafe-ai/sdk";
import Openquok from "@openquok/node-sdk";
import type { PublicCreatePostDto } from "@openquok/node-sdk";

type ChannelTargets = {
  threadsId: string;
  linkedInId?: string;
};

type InboundPostProposal = {
  globalCaption: string;
  /** Optional per-channel overrides when the author already supplied them */
  bodiesByChannel?: Partial<Record<"threads" | "linkedin", string>>;
  channelTargets: ChannelTargets;
};

function buildPostPayload(
  proposal: InboundPostProposal,
  composeMode: string,
): PublicCreatePostDto | null {
  const { globalCaption, bodiesByChannel, channelTargets } = proposal;
  const ids = [channelTargets.threadsId, channelTargets.linkedInId].filter(
    Boolean,
  ) as string[];

  const scheduledAt = new Date(Date.now() + 24 * 60 * 60 * 1000).toISOString();

  if (composeMode === "not_ready") return null;

  if (composeMode === "global_same_copy") {
    return {
      scheduledAt,
      status: "draft",
      body: globalCaption,
      integrationIds: ids,
      isGlobal: true,
      note: "Jev: global copy — review before scheduling",
      isAgent: true,
    };
  }

  if (composeMode === "per_channel_copy" && channelTargets.linkedInId) {
    return {
      scheduledAt,
      status: "draft",
      body: globalCaption,
      integrationIds: ids,
      isGlobal: false,
      bodiesByIntegrationId: {
        [channelTargets.threadsId]:
          bodiesByChannel?.threads ?? globalCaption,
        [channelTargets.linkedInId]:
          bodiesByChannel?.linkedin ??
          `${globalCaption}

Read more on our site.`,
      },
      note: "Jev: per-channel copy — review before scheduling",
      isAgent: true,
    };
  }

  if (composeMode === "global_copy_per_channel_settings") {
    return {
      scheduledAt,
      status: "draft",
      body: globalCaption,
      integrationIds: ids,
      isGlobal: true,
      providerSettingsByIntegrationId: channelTargets.linkedInId
        ? {
            [channelTargets.linkedInId]: {
              linkedin: { postAsImagesCarousel: false },
            },
          }
        : undefined,
      note: "Jev: global copy, per-channel settings only",
      isAgent: true,
    };
  }

  return null;
}

export async function evaluateAndCreateDraft(params: {
  proposal: InboundPostProposal;
  openquokAccessToken: string;
  relevanceFloor?: number;
}) {
  const { proposal, openquokAccessToken, relevanceFloor = 0.55 } = params;

  const jev = new TypeSafeClient();
  const decision = await jev.systemOne({
    state: {
      proposal: {
        caption: proposal.globalCaption,
        targets: Object.keys(proposal.channelTargets),
        perChannelBodies: proposal.bodiesByChannel ?? {},
      },
    },
    questions: {
      relevant: noul(
        "The post is relevant — it informs, entertains, or serves the audience",
      ),
      engaging: score("How engaging is this copy for social feeds?", [
        "Flat or generic — little reason to react",
        "Acceptable — clear but not compelling",
        "Strong — likely to earn likes or replies",
      ]),
      actionable: noul(
        "A reader would want to share, comment, or take a clear next step",
      ),
      needs_media: noul(
        "The post requires or strongly benefits from a photo or video asset",
      ),
      compose_mode: choice(
        "How should RedPanda compose this post?",
        {
          global_same_copy:
            "One caption and shared media for every selected channel",
          per_channel_copy:
            "Different caption or tone per network (e.g. casual Threads, formal LinkedIn)",
          global_copy_per_channel_settings:
            "Same caption everywhere; only platform settings differ (title, post type, tags)",
          not_ready: "Copy is not ready to schedule — needs human edit",
        },
      ),
    },
  });

  const { relevant, engaging, actionable, needs_media, compose_mode } =
    decision.answers;

  if (relevant.noul < relevanceFloor) {
    return { action: "human_review", reason: "Not relevant enough to post" };
  }

  if (engaging.score < 0.8 && engaging.confidence > 0.6) {
    return { action: "human_review", reason: "Low engagement — revise copy" };
  }

  if (actionable.noul < 0.35) {
    return { action: "human_review", reason: "Weak actionable signal" };
  }

  const payload = buildPostPayload(
    proposal,
    compose_mode.choice,
  );

  if (!payload) {
    return { action: "human_review", reason: "Jev marked post as not ready" };
  }

  if (needs_media.noul > 0.7 && !payload.media?.length) {
    payload.note = `${payload.note ?? ""} — attach media before scheduling`;
  }

  const openquok = new Openquok(openquokAccessToken, {
    baseUrl: process.env.OPENQUOK_API_URL ?? "https://api-production-a424.up.railway.app",
  });

  await openquok.isConnected();
  const created = await openquok.postAsAgent(payload);

  return {
    action: "draft_created",
    composeMode: compose_mode.choice,
    postGroup: (created as { data?: { postGroup?: string } })?.data?.postGroup,
  };
}

Recipe mapping

Each Jev compose_mode maps to an openquok-core recipe and Public API fields:

  • global_same_copy — threads-text-only.json — body, isGlobal: true
  • per_channel_copy — multi-platform-campaign.json — bodiesByIntegrationId, isGlobal: false
  • global_copy_per_channel_settings — youtube-video-title-privacy.json (settings-heavy) — shared body + providerSettingsByIntegrationId
  • Media required — threads-with-image.json — add media[] after openquok.upload()

Install recipes from CLI getting started. Build flows in Skill Builder.

Wire OAuth tokens per user

  1. Complete Node.js OAuth example and store each user’s opo_ token.
  2. Load channel UUIDs with openquok.integrations().
  3. Call evaluateAndCreateDraft with the user’s token and proposal.
  4. Surface human_review when Jev confidence is low or compose_mode is not_ready.

Single-workspace test:

export TYPESAFE_API_KEY="sk-..."
export OPENQUOK_API_KEY="opo_..."
export OPENQUOK_THREADS_INTEGRATION_ID="<integration-id>"
export OPENQUOK_LINKEDIN_INTEGRATION_ID="<integration-id>"
node jev-route-draft.mjs "Launch post: same news everywhere, but LinkedIn should sound more formal."

Confidence gates

Use stricter thresholds for creating RedPanda drafts than for ignoring noise. compose_mode confidence below your bar → ask a human to pick Global vs per-channel manually in the dashboard (Global vs per-channel).

ค้นหาเอกสาร
ค้นหาหน้าเอกสาร
ศูนย์ช่วยเหลือ