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.
Connect your agent today
Draft from chat, review in your calendar, and publish only what you approve.
Note
OpenQuok does not operate Jev. Jev is a decision model from TypeSafe AI. You install @typesafe-ai/sdk in your app and call OpenQuok with an opo_ token — from OAuth (this section) or from a workspace programmatic token. Nothing in OpenQuok connects to Jev automatically.
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:
- Decide — Is the post ready? Does it need media? Should it use Global mode (one caption everywhere) or per-channel overrides?
- Execute —
@openquok/node-sdkcreates a draft with the right shape (bodyonly, orbodiesByIntegrationId/providerSettingsByIntegrationId). - 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)
| Check | Question for Jev | OpenQuok follow-up |
|---|---|---|
| Relevant | Is this useful to the audience (inform, entertain, or community)? | Low score → human_review, do not create a draft |
| Engaging | Would someone want to read and react? | Very low → revise copy or discard |
| Actionable | Would someone share, comment, or click? | Optional gate for automated scheduling |
| Visuals | Does the post need photo or video? | needs_media high → draft with media[] or kanban note to attach assets |
| Channel fit | Same 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 mode | When to use | Public API shape |
|---|---|---|
| Global | Same announcement and attachments everywhere | body, media, integrationIds[], isGlobal: true |
| Per-channel copy | Different caption or media per network | bodiesByIntegrationId, optional mediaByIntegrationId |
| Global copy, per-channel settings | Same words; only YouTube title, Instagram post type, etc. differ | Shared body + providerSettingsByIntegrationId per UUID |
See Global vs per-channel and recipe multi-platform-campaign.json under agent/skills/openquok-core/resources/examples/.
Prerequisites
| Requirement | Notes |
|---|---|
| TYPESAFE_API_KEY | From console.typesafe.ai |
| opo_ access token | Per user after OAuth2 Authorization Code, or a workspace token for scripts |
| Channel UUIDs | From 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_copy—threads-text-only.json—body,isGlobal: trueper_channel_copy—multi-platform-campaign.json—bodiesByIntegrationId,isGlobal: falseglobal_copy_per_channel_settings—youtube-video-title-privacy.json(settings-heavy) — sharedbody+providerSettingsByIntegrationId- Media required —
threads-with-image.json— addmedia[]afteropenquok.upload()
Install recipes from CLI getting started. Build flows in Skill Builder.
Prefer drafts from automation
Automated pipelines should set status to draft. Teammates promote to scheduled after they confirm relevance, visuals, and per-channel fit in the composer.
Wire OAuth tokens per user
- Complete Node.js OAuth example and store each user’s opo_ token.
- Load channel UUIDs with
openquok.integrations(). - Call
evaluateAndCreateDraftwith the user’s token and proposal. - Surface human_review when Jev confidence is low or
compose_modeisnot_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).