npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@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/sdk

Quick 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:

  1. Your server creates a scoped client token using the API key (keeps it secret)
  2. Your browser uses HappyRobotChatClient with 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);
  }
}