@happyrobot-ai/sdk
v0.1.5
Published
TypeScript SDK for the HappyRobot Public API
Readme
@happyrobot-ai/sdk
TypeScript SDK for the HappyRobot Public API.
Installation
npm install @happyrobot-ai/sdk
# or
pnpm add @happyrobot-ai/sdk
# or
yarn add @happyrobot-ai/sdkQuick Start
import { HappyRobotClient } from "@happyrobot-ai/sdk";
const client = new HappyRobotClient({ apiKey: "sk_live_..." });
// List workflows
const { data } = await client.workflows.list();
// Trigger a run
const { run_id } = await client.workflows.triggerRun("my-workflow", {
payload: { phone: "+1234567890" },
});Configuration
const client = new HappyRobotClient({
apiKey: "sk_live_...", // Required. API key (sk_live_... or sk_test_...)
timeout: 30_000, // Optional. Request timeout in ms. Defaults to 30000
maxRetries: 2, // Optional. Retries on 429/5xx with exponential backoff. Defaults to 2
fetch: customFetch, // Optional. Custom fetch implementation
});Helpers
High-level convenience functions available via @happyrobot-ai/sdk/helpers.
triggerAndWait
Trigger a workflow run and poll until it reaches a terminal status.
import { triggerAndWait } from "@happyrobot-ai/sdk/helpers";
const { run, sessions } = await triggerAndWait(client, {
workflowId: "my-workflow",
payload: { phone: "+1234567890" },
timeoutMs: 300_000, // Optional. Defaults to 5 minutes
pollIntervalMs: 2_000, // Optional. Defaults to 2 seconds
fetchSessions: true, // Optional. Fetch sessions on completion. Defaults to true
environment: "production", // Optional. Defaults to "production"
});
console.log(run.status); // "completed" | "failed" | "canceled" | ...createVoiceAgent
Create a voice agent workflow from a template with optional publishing.
import { createVoiceAgent } from "@happyrobot-ai/sdk/helpers";
const { workflow, publishResult } = await createVoiceAgent(client, {
name: "Sales Agent",
template: "voice-agent", // "voice-agent" (outbound) or "inbound-voice-agent"
prompt: "You are a helpful sales assistant...",
initialMessage: "Hi, how can I help you today?",
publish: true,
environment: "production",
folderId: "folder-id", // Optional
});createFromTemplate
Create any workflow from a template with custom inputs.
import { createFromTemplate } from "@happyrobot-ai/sdk/helpers";
const { workflow } = await createFromTemplate(client, {
template: "voice-agent",
name: "My Agent",
inputs: { agent_name: "My Agent" },
publish: false,
});Resources
| Property | Class | Description |
|---|---|---|
| client.workflows | WorkflowsResource | Workflow CRUD, publishing, runs, and templates |
| client.versions | VersionsResource | Version management (fork, publish, lock, test) |
| client.nodes | NodesResource | Node CRUD, config schema, and available variables |
| client.runs | RunsResource | Run details, cancellation, and annotations |
| client.sessions | SessionsResource | Session details, messages, and SSE streaming |
| client.messages | MessagesResource | Message quality flags |
| client.variables | VariablesResource | Workflow-scoped variables |
| client.phoneNumbers | PhoneNumbersResource | Phone number management |
| client.sipTrunks | SipTrunksResource | SIP trunk management |
| client.integrations | IntegrationsResource | Integration and event discovery |
| client.contacts | ContactsResource | Contact lookup and history |
| client.knowledgeBases | KnowledgeBasesResource | Knowledge base document management |
| client.workflowFolders | WorkflowFoldersResource | Workflow folder organization |
| client.mcp | MCPResource | MCP server management |
| client.billing | BillingResource | Billing usage details and totals |
| client.apiKey | ApiKeyResource | API key introspection |
| client.artifacts | ArtifactsResource | Resolve fresh artifact URLs |
| client.adversarialSuites | AdversarialSuitesResource | Adversarial suite management and execution |
| client.adversarialTests | AdversarialTestsResource | Adversarial test management and execution |
| client.northstars | NorthstarsResource | Northstar quality criteria management |
| client.customEvals | CustomEvalsResource | Custom eval management and execution |
| client.issues | IssuesResource | Quality issue (flag) status management |
| client.auditRemarks | AuditRemarksResource | Audit remark feedback management |
| client.chat | ChatResource | Chat session management, messaging, and file uploads |
client.workflows
// List workflows (paginated)
const { data, pagination } = await client.workflows.list({ page: 1, limit: 20 });
// Iterate all workflows across pages
for await (const wf of client.workflows.listAll()) { ... }
// Create a workflow
const wf = await client.workflows.create({ name: "My Workflow" });
// Get by ID or slug
const wf = await client.workflows.get("my-workflow");
// Update
await client.workflows.update("my-workflow", { name: "New Name" });
// Delete
await client.workflows.delete("my-workflow");
// Duplicate
await client.workflows.duplicate("my-workflow", { name: "Copy" });
// Publish / unpublish
await client.workflows.publish("my-workflow");
await client.workflows.unpublish("my-workflow");
// Cancel all active runs
await client.workflows.cancelRuns("my-workflow");
// List templates
const { data } = await client.workflows.listTemplates();
// List versions
const { data } = await client.workflows.listVersions("my-workflow");
// List runs
const { data } = await client.workflows.listRuns("my-workflow");
// Trigger a run
const { run_id } = await client.workflows.triggerRun("my-workflow", { payload: { phone: "+1..." } });| Method | HTTP | Path | Description |
|---|---|---|---|
| list(query?) | GET | /workflows | List workflows, paginated |
| listAll(query?) | GET | /workflows | Async generator over all pages |
| create(body) | POST | /workflows | Create a new workflow |
| get(workflowId) | GET | /workflows/:id | Get workflow by ID or slug |
| update(workflowId, body) | PATCH | /workflows/:id | Update name, description, etc. |
| delete(workflowId) | DELETE | /workflows/:id | Delete a workflow |
| duplicate(workflowId, body?) | POST | /workflows/:id/duplicate | Duplicate a workflow |
| publish(workflowId, body?) | POST | /workflows/:id/publish | Publish the latest version |
| unpublish(workflowId) | POST | /workflows/:id/unpublish | Unpublish a workflow |
| cancelRuns(workflowId) | POST | /workflows/:id/cancel-runs | Cancel all active runs |
| listTemplates(query?) | GET | /workflows/templates | List workflow templates |
| listVersions(workflowId, query?) | GET | /workflows/:id/versions | List versions for a workflow |
| listRuns(workflowId, query?) | GET | /workflows/:id/runs | List runs for a workflow |
| triggerRun(workflowId, body?) | POST | /workflows/:id/runs | Trigger a new run |
client.versions
const version = await client.versions.get("version-id");
await client.versions.update("version-id", { name: "v2" });
await client.versions.fork("version-id");
await client.versions.publish("version-id");
await client.versions.unpublish("version-id");
await client.versions.lock("version-id");
await client.versions.unlock("version-id");
await client.versions.testAll("version-id");
const issues = await client.versions.getPromptIssues("version-id");| Method | HTTP | Path | Description |
|---|---|---|---|
| get(versionId) | GET | /versions/:id | Get version with node summary and changelog |
| update(versionId, body) | PATCH | /versions/:id | Update name or description |
| fork(versionId) | POST | /versions/:id/fork | Clone a version |
| publish(versionId, body?) | POST | /versions/:id/publish | Publish a version |
| unpublish(versionId) | POST | /versions/:id/unpublish | Unpublish a version |
| lock(versionId) | POST | /versions/:id/lock | Lock a version (prevent edits) |
| unlock(versionId) | POST | /versions/:id/unlock | Unlock a version |
| testAll(versionId) | POST | /versions/:id/test-all | Run test-all validation |
| getPromptIssues(versionId) | GET | /versions/:id/prompt-issues | Get prompt quality issues for all prompt nodes |
client.nodes
Nodes are always scoped to a version.
const nodes = await client.nodes.list("version-id");
await client.nodes.addBatch("version-id", { nodes: [...] });
const node = await client.nodes.get("version-id", "node-id");
await client.nodes.update("version-id", "node-id", { config: { ... } });
await client.nodes.delete("version-id", "node-id");
const schema = await client.nodes.getConfigSchema("version-id", "node-id");
const vars = await client.nodes.getAvailableVars("version-id", "node-id");
const result = await client.nodes.test("version-id", "node-id");| Method | HTTP | Path | Description |
|---|---|---|---|
| list(versionId) | GET | /versions/:vId/nodes | List all nodes in a version |
| addBatch(versionId, body) | POST | /versions/:vId/nodes | Add nodes in batch (1–50) |
| get(versionId, nodeId) | GET | /versions/:vId/nodes/:nId | Get a single node |
| update(versionId, nodeId, body) | PUT | /versions/:vId/nodes/:nId | Update a node |
| delete(versionId, nodeId) | DELETE | /versions/:vId/nodes/:nId | Delete a node |
| getConfigSchema(versionId, nodeId) | GET | /versions/:vId/nodes/:nId/config-schema | Get field types and requirements |
| getAvailableVars(versionId, nodeId) | GET | /versions/:vId/nodes/:nId/available-vars | Get upstream variables available to this node |
| test(versionId, nodeId, body?) | POST | /versions/:vId/nodes/:nId/test | Test a single node (version must not be published) |
client.runs
const run = await client.runs.get("run-id");
const sessions = await client.runs.getSessions("run-id");
const recordings = await client.runs.getRecordings("run-id");
const flags = await client.runs.getFlags("run-id");
await client.runs.cancel("run-id");
await client.runs.mark("run-id", { annotation: "correct" });| Method | HTTP | Path | Description |
|---|---|---|---|
| get(runId) | GET | /runs/:id | Get run details |
| getSessions(runId, query?) | GET | /runs/:id/sessions | Get sessions for a run |
| getRecordings(runId) | GET | /runs/:id/recordings | Get recordings for a run |
| getFlags(runId, query?) | GET | /runs/:id/flags | Get quality flags for a run |
| cancel(runId) | POST | /runs/:id/cancel | Cancel a run |
| mark(runId, body) | POST | /runs/:id/mark | Annotate a run (correct / incorrect / critical) |
client.sessions
const session = await client.sessions.get("session-id");
const { data } = await client.sessions.getMessages("session-id");
// SSE streaming
for await (const event of await client.sessions.stream("session-id")) {
if (event.event === "message") console.log(event.data.role, event.data.content);
if (event.event === "session_ended") break;
}| Method | HTTP | Path | Description |
|---|---|---|---|
| get(sessionId) | GET | /sessions/:id | Get session details |
| getMessages(sessionId, query?) | GET | /sessions/:id/messages | Get paginated messages for a session |
| stream(sessionId, query?) | GET | /sessions/:id/stream | Open SSE stream of session messages |
client.messages
const { data } = await client.messages.listFlags("message-id");
await client.messages.createFlag("message-id", { type: "incorrect", note: "..." });| Method | HTTP | Path | Description |
|---|---|---|---|
| listFlags(messageId, query?) | GET | /messages/:id/flags | List quality flags for a message |
| createFlag(messageId, body) | POST | /messages/:id/flags | Create a quality flag on a message |
client.artifacts
const resolved = await client.artifacts.resolve({
s3_keys: ["artifacts/<media_id>/extracted_text.txt"],
});| Method | HTTP | Path | Description |
|---|---|---|---|
| resolve(body) | POST | /artifacts/resolve | Resolve fresh presigned URLs for artifacts |
client.variables
Variables are scoped to a workflow.
const { data } = await client.variables.list("workflow-id");
await client.variables.create("workflow-id", { name: "MY_VAR", value: "hello" });
await client.variables.update("workflow-id", "variable-id", { value: "world" });
await client.variables.delete("workflow-id", "variable-id");| Method | HTTP | Path | Description |
|---|---|---|---|
| list(workflowId, query?) | GET | /workflows/:wId/variables | List variables for a workflow |
| create(workflowId, body) | POST | /workflows/:wId/variables | Create a variable |
| update(workflowId, variableId, body) | PATCH | /workflows/:wId/variables/:vId | Update a variable |
| delete(workflowId, variableId) | DELETE | /workflows/:wId/variables/:vId | Delete a variable |
client.phoneNumbers
const numbers = await client.phoneNumbers.list();
await client.phoneNumbers.buy({ area_code: "415" });
await client.phoneNumbers.update("id", { name: "Main Line" });
await client.phoneNumbers.getUsage("id");
await client.phoneNumbers.freeUp("id");
await client.phoneNumbers.delete("id");
await client.phoneNumbers.removeFromWorkflow("id", { use_case_id: "..." });
await client.phoneNumbers.getTollFreeVerification("id");
await client.phoneNumbers.createTollFreeVerification("id", { ... });
await client.phoneNumbers.deleteTollFreeVerification("verification-sid");
await client.phoneNumbers.createSipTrunk("id");
await client.phoneNumbers.validateTollFreeNumbers({ phone_numbers: [...] });| Method | HTTP | Path | Description |
|---|---|---|---|
| list(query?) | GET | /phone-numbers | List all phone numbers |
| buy(body) | POST | /phone-numbers | Purchase a new phone number |
| update(id, body) | PUT | /phone-numbers/:id | Update name or caller ID |
| getUsage(id) | GET | /phone-numbers/:id/usage | Get usage info |
| getTollFreeVerification(id) | GET | /phone-numbers/:id/tollfree-verification | Get toll-free verification status |
| createTollFreeVerification(id, body) | POST | /phone-numbers/:id/tollfree-verification | Start toll-free verification |
| deleteTollFreeVerification(verificationSid) | DELETE | /phone-numbers/tollfree-verification/:sid | Delete a toll-free verification |
| createSipTrunk(id) | POST | /phone-numbers/:id/sip-trunk | Create a SIP trunk for a number |
| freeUp(id) | POST | /phone-numbers/:id/free-up-number | Release a number from workflows |
| delete(id) | POST | /phone-numbers/:id/delete-number | Permanently delete a number |
| removeFromWorkflow(id, body) | POST | /phone-numbers/:id/remove-from-workflow | Remove from a specific workflow |
| validateTollFreeNumbers(body) | POST | /phone-numbers/validate-toll-free-numbers | Check for conflicts |
client.sipTrunks
const trunks = await client.sipTrunks.list();
const options = await client.sipTrunks.listOptions();
await client.sipTrunks.create({ name: "Main Trunk", ... });
await client.sipTrunks.createBulk({ trunks: [...] });
const trunk = await client.sipTrunks.get("trunk-id");
await client.sipTrunks.update("trunk-id", { name: "Updated" });
await client.sipTrunks.delete("trunk-id");| Method | HTTP | Path | Description |
|---|---|---|---|
| list() | GET | /sip-trunks | List all SIP trunks |
| listOptions() | GET | /sip-trunks/options | Lightweight list of trunk options |
| create(body) | POST | /sip-trunks | Create a SIP trunk |
| createBulk(body) | POST | /sip-trunks/bulk | Create multiple SIP trunks |
| get(trunkId) | GET | /sip-trunks/:id | Get a SIP trunk |
| update(trunkId, body) | PUT | /sip-trunks/:id | Update a SIP trunk |
| delete(trunkId) | DELETE | /sip-trunks/:id | Delete a SIP trunk |
client.integrations
const integrations = await client.integrations.list();
const integration = await client.integrations.get("integration-id");
await client.integrations.createCredential("integration-id", { name: "My Creds", fields: { ... } });
// Integration-specific sub-resources
await client.integrations.googleSheets.spreadsheets();
await client.integrations.googleSheets.worksheets({ spreadsheet_id: "..." });
await client.integrations.googleSheets.columns({ spreadsheet_id: "...", worksheet_id: "..." });
await client.integrations.googleSheets.rows({ spreadsheet_id: "...", worksheet_id: "..." });
await client.integrations.slack.channels();
await client.integrations.slack.users();
await client.integrations.teams.teams();
await client.integrations.teams.channels();
await client.integrations.teams.users();
await client.integrations.twilioSms.phoneNumbers();
await client.integrations.whatsApp.businesses({ credential_id: "..." });
await client.integrations.whatsApp.businessAccounts({ credential_id: "...", business_id: "..." });
await client.integrations.whatsApp.phoneNumbers({ credential_id: "...", business_account_id: "..." });
await client.integrations.whatsApp.messageTemplates({ credential_id: "...", business_account_id: "..." });| Method | HTTP | Path | Description |
|---|---|---|---|
| list(query?) | GET | /integrations | List all integrations |
| get(id) | GET | /integrations/:id | Get integration and its events |
| createCredential(id, body) | POST | /integrations/:id/create-credential | Create a credential for a form-based integration |
| googleSheets.spreadsheets(query?) | GET | /integrations/google-sheets/spreadsheets | List spreadsheets |
| googleSheets.worksheets(query) | GET | /integrations/google-sheets/worksheets | List worksheets in a spreadsheet |
| googleSheets.columns(query) | GET | /integrations/google-sheets/columns | List columns in a worksheet |
| googleSheets.rows(query) | GET | /integrations/google-sheets/rows | List rows in a worksheet |
| slack.channels(query?) | GET | /integrations/slack/channels | List Slack channels |
| slack.users(query?) | GET | /integrations/slack/users | List Slack users |
| teams.teams(query?) | GET | /integrations/teams/teams | List Microsoft Teams teams |
| teams.channels(query?) | GET | /integrations/teams/channels | List Microsoft Teams channels |
| teams.users(query?) | GET | /integrations/teams/users | List Microsoft Teams users |
| twilioSms.phoneNumbers(query?) | GET | /integrations/twilio-sms/phone-numbers | List Twilio SMS numbers |
| whatsApp.businesses(query) | GET | /integrations/whatsapp/businesses | List WhatsApp businesses |
| whatsApp.businessAccounts(query) | GET | /integrations/whatsapp/business-accounts | List WhatsApp business accounts |
| whatsApp.phoneNumbers(query) | GET | /integrations/whatsapp/phone-numbers | List WhatsApp phone numbers |
| whatsApp.messageTemplates(query) | GET | /integrations/whatsapp/message-templates | List WhatsApp message templates |
client.contacts
const { data } = await client.contacts.list({ search: "John" });
const contact = await client.contacts.resolve({ phone_number: "+14155551234" });
const contact = await client.contacts.get("contact-id");
const interactions = await client.contacts.getInteractions("contact-id");
const memories = await client.contacts.getMemories("contact-id");| Method | HTTP | Path | Description |
|---|---|---|---|
| list(query?) | GET | /contacts | List contacts (cursor-paginated) |
| resolve(query) | GET | /contacts/resolve | Look up a contact by phone number or email |
| get(contactId) | GET | /contacts/:id | Get contact by ID |
| getInteractions(contactId) | GET | /contacts/:id/interactions | Get call/message history for a contact |
| getMemories(contactId) | GET | /contacts/:id/memories | Get AI memories for a contact |
client.knowledgeBases
const kbs = await client.knowledgeBases.list();
const files = await client.knowledgeBases.listFiles("kb-id");
const urls = await client.knowledgeBases.getUploadUrls("kb-id", { files: [{ name: "doc.pdf", content_type: "application/pdf" }] });
await client.knowledgeBases.triggerChunking("kb-id");
await client.knowledgeBases.deleteFile("kb-id", "file-id");
await client.knowledgeBases.delete("kb-id");| Method | HTTP | Path | Description |
|---|---|---|---|
| list() | GET | /knowledge-bases | List all knowledge bases |
| listFiles(kbId) | GET | /knowledge-bases/:id/files | List documents in a knowledge base |
| getUploadUrls(kbId, body) | POST | /knowledge-bases/:id/upload-urls | Get pre-signed S3 upload URLs |
| triggerChunking(kbId) | POST | /knowledge-bases/:id/trigger-chunking | Start document processing/chunking |
| deleteFile(kbId, fileId) | DELETE | /knowledge-bases/:id/files/:fileId | Delete a file from a knowledge base |
| delete(kbId) | DELETE | /knowledge-bases/:id | Delete a knowledge base |
client.workflowFolders
const { data } = await client.workflowFolders.list();
await client.workflowFolders.create({ name: "Production" });
const folder = await client.workflowFolders.get("folder-id");
await client.workflowFolders.update("folder-id", { name: "Updated" });
await client.workflowFolders.delete("folder-id");| Method | HTTP | Path | Description |
|---|---|---|---|
| list(query?) | GET | /workflow-folders | List folders (paginated) |
| create(body) | POST | /workflow-folders | Create a folder |
| get(folderId) | GET | /workflow-folders/:id | Get a folder |
| update(folderId, body) | PUT | /workflow-folders/:id | Update a folder |
| delete(folderId) | DELETE | /workflow-folders/:id | Delete a folder |
client.mcp
const { data } = await client.mcp.list();
await client.mcp.create({ name: "My MCP", url: "https://..." });
await client.mcp.refresh("mcp-id");| Method | HTTP | Path | Description |
|---|---|---|---|
| list(query?) | GET | /mcp | List MCP servers (paginated) |
| create(body) | POST | /mcp | Register a new MCP server |
| refresh(mcpId) | POST | /mcp/:id/refresh | Re-discover tools for an MCP server |
client.billing
const details = await client.billing.getDetails({ start: "2024-01-01", end: "2024-01-31" });
const totals = await client.billing.getTotals({ start: "2024-01-01", end: "2024-01-31" });| Method | HTTP | Path | Description |
|---|---|---|---|
| getDetails(query) | GET | /billing/usage/details | Detailed billing line items |
| getTotals(query) | GET | /billing/usage/totals | Aggregated billing totals |
client.apiKey
const info = await client.apiKey.describe();| Method | HTTP | Path | Description |
|---|---|---|---|
| describe() | GET | /api-key/describe | Introspect the current API key (ID, name, org, etc.) |
client.adversarialSuites
Adversarial suites group multiple adversarial tests and can auto-generate them from a prompt.
const { suite } = await client.adversarialSuites.get("suite-id");
await client.adversarialSuites.update("suite-id", { name: "New Name", generation_count: 10 });
await client.adversarialSuites.delete("suite-id");
// Generate tests from the suite's generation_prompt using AI
await client.adversarialSuites.generate("suite-id", { version_id: "v-id" });
// Generate a mermaid workflow graph for visualization
await client.adversarialSuites.generateGraph("suite-id", { version_id: "v-id" });
// Run all tests in the suite
const { suite_run_id } = await client.adversarialSuites.run("suite-id", { version_id: "v-id" });
// Poll results
const { runs } = await client.adversarialSuites.listRuns("suite-id");
const { run } = await client.adversarialSuites.getRun("suite-run-id");
const { test_runs } = await client.adversarialSuites.getRunTestRuns("suite-run-id");| Method | HTTP | Path | Description |
|---|---|---|---|
| get(suiteId) | GET | /adversarial-suites/:id | Get suite with optional mermaid graph |
| update(suiteId, body) | PATCH | /adversarial-suites/:id | Update name, model, timeout, generation settings |
| delete(suiteId) | DELETE | /adversarial-suites/:id | Delete a suite |
| generate(suiteId, body?) | POST | /adversarial-suites/:id/generate | AI-generate tests from the suite's generation prompt |
| generateGraph(suiteId, body) | POST | /adversarial-suites/:id/generate-graph | Generate a mermaid workflow visualization |
| run(suiteId, body) | POST | /adversarial-suites/:id/run | Execute all tests asynchronously, returns suite_run_id |
| listRuns(suiteId, query?) | GET | /adversarial-suites/:id/runs | List suite runs |
| getRun(suiteRunId) | GET | /adversarial-suites/runs/:runId | Get a suite run with aggregate pass/fail counts |
| getRunTestRuns(suiteRunId) | GET | /adversarial-suites/runs/:runId/test-runs | List individual test results within a suite run |
client.adversarialTests
Individual adversarial tests simulate a user trying to break the agent's behavior.
const { test } = await client.adversarialTests.get("test-id");
await client.adversarialTests.update("test-id", { adversarial_prompt: "Try to get PII", timeout_seconds: 120 });
await client.adversarialTests.delete("test-id");
// Run a single test
const { test_run_id } = await client.adversarialTests.run("test-id", { version_id: "v-id" });
// Poll results
const { runs } = await client.adversarialTests.listRuns("test-id");
const { run } = await client.adversarialTests.getRun("run-id");
const { messages } = await client.adversarialTests.getRunMessages("run-id");| Method | HTTP | Path | Description |
|---|---|---|---|
| get(testId) | GET | /adversarial-tests/:id | Get test details |
| update(testId, body) | PATCH | /adversarial-tests/:id | Update prompt, model, variables, timeout |
| delete(testId) | DELETE | /adversarial-tests/:id | Delete a test |
| run(testId, body) | POST | /adversarial-tests/:id/run | Execute a test asynchronously, returns test_run_id |
| listRuns(testId, query?) | GET | /adversarial-tests/:id/runs | List test runs with audit remarks |
| getRun(runId) | GET | /adversarial-tests/runs/:runId | Get a single run with full audit remarks |
| getRunMessages(runId) | GET | /adversarial-tests/runs/:runId/messages | Get the full conversation from a test run |
client.northstars
Northstars are quality criteria that define how the agent should behave. They are used to automatically grade conversations.
const { northstar } = await client.northstars.get("northstar-id");
await client.northstars.update("northstar-id", { enabled: true, priority: "high" });
await client.northstars.delete("northstar-id");
// Get full regeneration history
const { history } = await client.northstars.getHistory("northstar-id");
// Rate a northstar (-2 = strongly wrong, +2 = strongly correct)
const { feedback } = await client.northstars.submitFeedback("northstar-id", {
correctness: 2,
feedback: "This criterion is perfect.",
trigger_regeneration: false,
});
await client.northstars.deleteFeedback("northstar-id");| Method | HTTP | Path | Description |
|---|---|---|---|
| get(northstarId) | GET | /northstars/:id | Get a northstar |
| update(northstarId, body) | PATCH | /northstars/:id | Update name, description, examples, category, priority, or enabled state |
| delete(northstarId) | DELETE | /northstars/:id | Delete a northstar |
| getHistory(northstarId) | GET | /northstars/:id/history | Get the full regeneration chain, oldest first |
| submitFeedback(northstarId, body) | POST | /northstars/:id/feedback | Rate a northstar's correctness (−2 to +2), optionally trigger regeneration |
| deleteFeedback(northstarId) | DELETE | /northstars/:id/feedback | Remove your feedback on a northstar |
client.customEvals
Custom evals test a specific prompt node against expected outputs or northstar criteria.
const { test } = await client.customEvals.get("eval-id");
await client.customEvals.update("eval-id", {
name: "Greeting check",
expected_response: "Hello, how can I help you?",
});
await client.customEvals.delete("eval-id");
// Run an eval
const { run_id } = await client.customEvals.run("eval-id", { version_id: "v-id" });
// Poll results
const { runs } = await client.customEvals.listRuns("eval-id");| Method | HTTP | Path | Description |
|---|---|---|---|
| get(evalId) | GET | /custom-evals/:id | Get a custom eval |
| update(evalId, body) | PATCH | /custom-evals/:id | Update messages, expected outputs, variables, or northstar IDs |
| delete(evalId) | DELETE | /custom-evals/:id | Delete a custom eval |
| run(evalId, body) | POST | /custom-evals/:id/run | Execute the eval asynchronously, returns run_id |
| listRuns(evalId, query?) | GET | /custom-evals/:id/runs | List runs with pass/fail and judge reasoning |
client.issues
Issues (quality flags) are raised when a conversation fails a northstar or quality check.
await client.issues.update("issue-id", { status: "approved" });| Method | HTTP | Path | Description |
|---|---|---|---|
| update(issueId, body) | PATCH | /issues/:id | Update issue status (open / approved / rejected / closed) |
client.auditRemarks
Audit remarks are individual northstar grades attached to a conversation. Feedback on audit remarks improves future grading.
const feedback = await client.auditRemarks.getFeedback("audit-remark-id");
// Thumbs up — automatically adds the remark as a northstar positive example
await client.auditRemarks.submitFeedback("audit-remark-id", {
polarity: true,
});
// Thumbs down with attribution
await client.auditRemarks.submitFeedback("audit-remark-id", {
polarity: false,
issue_attribution: "northstar",
comment: "The northstar criteria was wrong here.",
});
await client.auditRemarks.deleteFeedback("audit-remark-id");| Method | HTTP | Path | Description |
|---|---|---|---|
| getFeedback(auditRemarkId) | GET | /audit-remarks/:id/feedback | Get your feedback for this remark, or null |
| submitFeedback(auditRemarkId, body) | POST | /audit-remarks/:id/feedback | Submit thumbs up/down; a thumbs-up adds the remark as a northstar example |
| deleteFeedback(auditRemarkId) | DELETE | /audit-remarks/:id/feedback | Delete your feedback |
Chat (client.chat + HappyRobotChatClient)
Build custom chat UIs powered by your workflows. Uses a two-tier auth model:
- Your server creates a scoped client token using the API key (keeps it secret)
- Your browser uses
HappyRobotChatClientwith that token for all chat operations
Server-side: Create a client token
// --- YOUR SERVER (e.g. Next.js API route, Express handler) ---
import { HappyRobotClient } from "@happyrobot-ai/sdk";
const client = new HappyRobotClient({ apiKey: process.env.HAPPYROBOT_API_KEY });
app.post("/api/chat-token", async (req, res) => {
const { token, expires_at } = await client.chat.createToken({
workflow_id: "your-workflow-id",
});
res.json({ token, expires_at });
});Browser-side: HappyRobotChatClient
// --- YOUR BROWSER CODE (React, Vue, vanilla JS, etc.) ---
import { HappyRobotChatClient } from "@happyrobot-ai/sdk";
// 1. Get a client token from your server
const { token } = await fetch("/api/chat-token", { method: "POST" }).then(r => r.json());
// 2. Initialize the browser-side client
const chat = new HappyRobotChatClient({ token });
// 3. Create a session
const { session_id } = await chat.createSession();
// 4. Connect bidirectional WebSocket
const connection = chat.connect(session_id, {
onResponseStart: () => {
// Show typing indicator
},
onResponseChunk: (content) => {
// Append streamed text to the UI
},
onResponseEnd: (content) => {
// Full response complete — hide typing indicator
},
onSessionClosed: (event) => {
// Session ended: event.status, event.reason, event.duration
},
onTokenExpired: () => {
// Token expired — fetch a new token from your server and reconnect
},
});
// 5. Send messages through the WebSocket
const ack = await connection.sendMessage({ content: "Hello!" });
console.log(ack.message.id); // server-assigned message ID
// 6. Send messages with file attachments
const artifact = await chat.uploadFile(fileBlob, "photo.png", "image/png");
await connection.sendMessage({ content: "Here's the photo", artifacts: [artifact] });
// 7. Get history (page reload, reconnect)
const { messages } = await chat.getHistory(session_id);
// 8. Clean up
connection.close();Browser-side: File uploads
// Option A: Use the convenience method (handles all 3 steps)
const artifact = await chat.uploadFile(fileBlob, "photo.png", "image/png");
await connection.sendMessage({
content: "Here's the photo",
artifacts: [artifact],
});
// Option B: Manual 3-step flow for more control
const upload = await chat.getPresignedUpload({ filename: "photo.png", mime_type: "image/png" });
await fetch(upload.upload_url, { method: "PUT", body: fileBlob, headers: { "Content-Type": "image/png" } });
await chat.completeUpload({
artifact_id: upload.artifact_id,
s3_uri: upload.s3_uri,
filename: "photo.png",
mime_type: "image/png",
size_bytes: fileBlob.size,
});
await connection.sendMessage({
content: "Here's the photo",
artifacts: [{ media_id: upload.artifact_id, mime_type: "image/png", filename: "photo.png", size_bytes: fileBlob.size }],
});HappyRobotClient (server-side)
| Method | Description |
|---|---|
| client.chat.createToken({ workflow_id, env? }) | Create a scoped client token (1 hour expiry) |
HappyRobotChatClient (browser-side)
| Method | Description |
|---|---|
| chat.createSession() | Create a new chat session |
| chat.connect(sessionId, handlers) | Open bidirectional WebSocket — returns ChatConnection |
| chat.sendMessage(sessionId, { content, artifacts? }) | Send a user message via HTTP (fallback if WS unavailable) |
| chat.getHistory(sessionId) | Get message history |
| chat.uploadFile(file, filename, mimeType) | Upload a file (convenience method) |
| chat.getPresignedUpload({ filename, mime_type }) | Get presigned S3 upload URL |
| chat.completeUpload({ artifact_id, s3_uri, ... }) | Register uploaded artifact |
ChatConnection
| Method | Description |
|---|---|
| connection.sendMessage({ content, artifacts? }) | Send a message via WebSocket. Returns a Promise that resolves with the server ack |
| connection.close() | Close the WebSocket connection |
| connection.ws | The underlying WebSocket instance |
WebSocket Events
Server → Client:
| Event | Fields | Description |
|---|---|---|
| connected | session_id | Connection established |
| response-start | content | AI started generating a response |
| response-chunk | content | Partial response text |
| response-end | content | Complete response text |
| session-closed | session_id, status, reason, duration, timestamp | Session ended |
| token-expired | — | JWT expired — reconnect with a fresh token |
| message-ack | id, message | Server confirmed the message was sent |
| message-error | id, error | Server failed to send the message |
| heartbeat | — | Keep-alive (every 15s) |
Client → Server:
| Event | Fields | Description |
|---|---|---|
| message | content, artifacts?, id? | Send a user message. id is echoed back in ack/error |
Pagination
List methods that are paginated return a PaginatedResponse<T>:
interface PaginatedResponse<T> {
data: T[];
pagination: {
page: number;
page_size: number;
total_pages: number;
total_records: number;
has_next_page: boolean;
has_previous_page: boolean;
};
}For resources that support listAll(), use the async generator to iterate all pages automatically:
for await (const workflow of client.workflows.listAll({ folder_id: "..." })) {
console.log(workflow.name);
}Error Handling
import { ApiError, AuthenticationError, NotFoundError } from "@happyrobot-ai/sdk";
try {
const wf = await client.workflows.get("nonexistent");
} catch (err) {
if (err instanceof NotFoundError) {
console.log("Workflow not found");
} else if (err instanceof AuthenticationError) {
console.log("Invalid API key");
} else if (err instanceof ApiError) {
console.log(err.status, err.message);
}
}