// Versioned seed for the canonical multi-persona interaction story. // // Scenario: a new hire ("Priya Sahota") needs onboarding. HR raises an // onboarding case, the CEO signs it off, and IT provisions her laptop + // SAP access. Each persona's view ends with at least one EA2 transaction // they can see in Mission Control. // // Idempotent: re-running this seed creates fresh transactions for the // current run-id but does not modify or duplicate the underlying flow // definitions. Run-id prefix is `canvas-personas-`. import { execSync } from "node:child_process"; const BASE = "https://demo.flow-master.ai"; const RUN_ID = `canvas-personas-${new Date().toISOString().slice(0, 19).replace(/[-:T]/g, "")}`; const PERSONAS = { ceo: "ceo-head@flow-master.ai", hr: "hr-head@flow-master.ai", it: "it-head@flow-master.ai", }; async function devLogin(email) { const r = await fetch(`${BASE}/api/v1/auth/dev-login`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ email }), }); if (!r.ok) throw new Error(`dev-login ${email}: ${r.status}`); return (await r.json()).access_token; } async function listPublished(token) { const r = await fetch(`${BASE}/api/ea2/flow/processes?limit=500`, { headers: { Authorization: `Bearer ${token}` }, }); if (!r.ok) throw new Error(`processes: ${r.status}`); const { items } = await r.json(); return items.filter((it) => it.status === "published" && it.kind === "definition"); } function pickFlow(items, predicate) { return items.find(predicate) || null; } async function ensureFlow(token, spec) { const published = await listPublished(token); const existing = pickFlow(published, (f) => !!f.display_name && new RegExp(spec.match, "i").test(`${f.display_name} ${f.description || ""} ${f.name || ""}`) ); if (existing) return existing; const uniqueName = `${spec.name}_${Date.now().toString(36)}`; const create = await fetch(`${BASE}/api/ea2/flow`, { method: "POST", headers: { "Content-Type": "application/json", Authorization: `Bearer ${token}` }, body: JSON.stringify({ kind: "definition", status: "draft", name: uniqueName, display_name: spec.display_name, description: spec.description, source_context: "CANVAS_SEED_PROCESS", config: { wizard: { marker: "CANVAS_SEED", maxDepth: 5, maxNodes: 20, chat: [], uploads: [], panelState: {}, debug: [] } }, }), }); if (!create.ok) { const txt = await create.text(); throw new Error(`ensureFlow create ${spec.display_name}: ${create.status} ${txt.slice(0, 200)}`); } const parent = await create.json(); if (!parent._key) throw new Error(`ensureFlow no _key in response: ${JSON.stringify(parent).slice(0, 200)}`); const reqId = `${RUN_ID}-${spec.name}-${Math.random().toString(36).slice(2, 10)}`; const stepOps = spec.steps.map((s) => { const slug = s.toLowerCase().replace(/[^a-z0-9]+/g, "_").slice(0, 40) || "step"; return { op: "create", coll: "flow", data: { kind: "definition", status: "draft", name: slug, display_name: s, description: s, source_context: "CANVAS_SEED_STEP", dispatch_kind: "human", }, }; }); const createRes = await fetch(`${BASE}/api/ea2/apply-batch`, { method: "POST", headers: { "Content-Type": "application/json", Authorization: `Bearer ${token}` }, body: JSON.stringify({ request_id: reqId, ops: stepOps }), }); const createJson = await createRes.json(); const stepKeys = (createJson?.result?.ops || []).map((o) => o.key); const linkOps = [ { op: "create_edge", edge_coll: "defines", from: `flow/${parent._key}`, to: `flow/${stepKeys[0]}`, role: "next" }, ...stepKeys.slice(0, -1).map((k, i) => ({ op: "create_edge", edge_coll: "defines", from: `flow/${k}`, to: `flow/${stepKeys[i + 1]}`, role: "next", })), ]; await fetch(`${BASE}/api/ea2/apply-batch`, { method: "POST", headers: { "Content-Type": "application/json", Authorization: `Bearer ${token}` }, body: JSON.stringify({ request_id: `${reqId}-edges`, ops: linkOps }), }); await fetch(`${BASE}/api/ea2/apply-batch`, { method: "POST", headers: { "Content-Type": "application/json", Authorization: `Bearer ${token}` }, body: JSON.stringify({ request_id: `${reqId}-publish`, ops: [{ op: "update", coll: "flow", key: parent._key, data: { status: "published" } }] }), }); return { ...parent, status: "published" }; } async function startInstance(token, defKey, businessSubject) { const r = await fetch(`${BASE}/api/runtime/transactions`, { method: "POST", headers: { "Content-Type": "application/json", Authorization: `Bearer ${token}` }, body: JSON.stringify({ process_definition_id: defKey, business_subject: businessSubject }), }); if (!r.ok) { const txt = await r.text(); throw new Error(`start ${defKey} for ${businessSubject}: ${r.status} ${txt.slice(0, 200)}`); } return r.json(); } async function sendChat(token, fromEmail, toEmail, body) { const threadDoc = await fetch(`${BASE}/api/ea2/flow`, { method: "POST", headers: { "Content-Type": "application/json", Authorization: `Bearer ${token}` }, body: JSON.stringify({ kind: "value", status: "published", name: `seed_chat_${Date.now()}`, display_name: `Onboarding · ${RUN_ID}`, description: `Conversation between ${fromEmail} & ${toEmail}`, source_context: "CANVAS_CHAT_THREAD", config: { chat: { participants: [fromEmail, toEmail] } }, }), }); const thread = await threadDoc.json(); const msgDoc = await fetch(`${BASE}/api/ea2/flow`, { method: "POST", headers: { "Content-Type": "application/json", Authorization: `Bearer ${token}` }, body: JSON.stringify({ kind: "value", status: "published", name: `seed_msg_${Date.now()}`, display_name: body.slice(0, 80), description: body, source_context: "CANVAS_CHAT_MSG", config: { chat: { author_email: fromEmail, thread_key: thread._key } }, }), }); const msg = await msgDoc.json(); const reqId = `${RUN_ID}-${Math.random().toString(36).slice(2, 12)}`; await fetch(`${BASE}/api/ea2/apply-batch`, { method: "POST", headers: { "Content-Type": "application/json", Authorization: `Bearer ${token}` }, body: JSON.stringify({ request_id: reqId, ops: [{ op: "create_edge", edge_coll: "defines", from: `flow/${thread._key}`, to: `flow/${msg._key}`, role: "next" }], }), }); return { thread_key: thread._key, msg_key: msg._key }; } const log = (...a) => console.log("[interactions]", ...a); const hrToken = await devLogin(PERSONAS.hr); log(`HR signed in (${PERSONAS.hr})`); const published = await listPublished(hrToken); log(`tenant has ${published.length} published flows`); const approvalCatalogue = published.find((f) => f._key === "pr_to_po_def") || published.find((f) => /procurement|requisition.*po|pr.to.po/i.test(`${f.display_name || ""} ${f.name || ""}`)); if (!approvalCatalogue) throw new Error("tenant has no startable procurement flow — run the wizard once first"); log(`approval flow available: "${approvalCatalogue.display_name || approvalCatalogue.name}" (${approvalCatalogue._key})`); const onboardTx = await startInstance(hrToken, approvalCatalogue._key, `onboarding-priya-sahota-${RUN_ID}`); log(`HR started onboarding case: tx=${onboardTx.transaction_id || onboardTx.short_id || "?"}`); const hrToCeoChat = await sendChat( hrToken, PERSONAS.hr, PERSONAS.ceo, `Heads up — onboarding new hire Priya Sahota for the Manchester store. Need your sign-off on the laptop budget (£1,400).` ); log(`HR -> CEO chat thread=${hrToCeoChat.thread_key}`); const ceoToken = await devLogin(PERSONAS.ceo); log(`CEO signed in (${PERSONAS.ceo})`); const ceoTx = await startInstance(ceoToken, approvalCatalogue._key, `laptop-budget-priya-${RUN_ID}`); log(`CEO started budget approval case: tx=${ceoTx.transaction_id || ceoTx.short_id || "?"}`); const ceoToHrChat = await sendChat( ceoToken, PERSONAS.ceo, PERSONAS.hr, `Approved up to £1,500 for Priya's kit. Loop in IT for the SAP roles.` ); log(`CEO -> HR chat thread=${ceoToHrChat.thread_key}`); const itToken = await devLogin(PERSONAS.it); log(`IT signed in (${PERSONAS.it})`); const itTx = await startInstance(itToken, approvalCatalogue._key, `it-provision-priya-${RUN_ID}`); log(`IT started provisioning case: tx=${itTx.transaction_id || itTx.short_id || "?"}`); const itToHrChat = await sendChat( itToken, PERSONAS.it, PERSONAS.hr, `Laptop ordered, SAP profile ready for day one. ETA Friday.` ); log(`IT -> HR chat thread=${itToHrChat.thread_key}`); console.log("\nmulti-persona seed complete."); console.log(`run id: ${RUN_ID}`); console.log(`onboarding tx (HR): ${onboardTx.transaction_id || onboardTx.short_id || "?"}`); console.log(`approval tx (CEO): ${ceoTx.transaction_id || ceoTx.short_id || "?"}`); console.log(`provision tx (IT): ${itTx.transaction_id || itTx.short_id || "?"}`);