Skip to content

Jev decision routing example

Use Jev to decide what to post — relevant, engaging, actionable, and channel-fit — then create OpenQuok social scheduler drafts in Global or per-channel mode via @openquok/node-sdk.

4 min read

Connect your agent today

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

Start for $0

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 OpenQuok 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 JevOpenQuok 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. OpenQuok 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 OpenQuok 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.openquok.com",
  });

  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_copythreads-text-only.jsonbody, isGlobal: true
  • per_channel_copymulti-platform-campaign.jsonbodiesByIntegrationId, isGlobal: false
  • global_copy_per_channel_settingsyoutube-video-title-privacy.json (settings-heavy) — shared body + providerSettingsByIntegrationId
  • Media requiredthreads-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 OpenQuok 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).

Search documentation
Find a docs page
Discord Support