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

@japan-ai-inc/studio-sdk

v0.7.0

Published

TypeScript SDK for Japan AI Studio platform APIs

Readme

@japan-ai-inc/studio-sdk

TypeScript SDK for the Japan AI Studio API. Provides typed, ergonomic access to Agents, Custom Objects, Storage, Workflows, and the Pages Rendering Service (PRS) platform.

Installation

Node.js 22 or later is required for the Node SDK and CLI.

npm install @japan-ai-inc/studio-sdk

CLI (jai-studio)

Install the published CLI globally:

npm install --global @japan-ai-inc/studio-sdk

Install the coding-agent skill

The npm package includes a version-matched studio_sdk skill for Codex and Claude Code. Installation is local: it does not require gh, a GitHub account, or a network request.

# Interactive agent and scope selection
jai-studio skill install

# Non-interactive examples
jai-studio skill install --agent codex --scope project
jai-studio skill install --agent claude-code --scope user
jai-studio skill install --agent all --scope project

Project scope installs under .agents/skills/studio_sdk for Codex or .claude/skills/studio_sdk for Claude Code. User scope installs under the corresponding directory in the user's home. Re-running an identical install is a no-op; a different existing copy is preserved unless --force is explicit. Start a new coding-agent session after installation so the agent discovers the skill.

Configure a profile

# Interactive prompt (recommended — key never appears in shell history)
jai-studio config set my-project

# Piped from a secret manager (CI/automation)
vault read -field=key secret/jai | jai-studio config set my-project --key-stdin

# Environment variable
JAI_STUDIO_API_KEY=pak_... jai-studio config set my-project

Security: Avoid passing API keys as CLI arguments — they are visible in shell history and process listings. Use the interactive prompt, --key-stdin, or the JAI_STUDIO_API_KEY environment variable instead.

Profiles belong to the CLI. createClient does not read them, and config show prints the stored key masked as <first 8>...<last 4>, so that output cannot be piped into SDK code — the client rejects a masked key with an explicit error instead of letting the server answer Invalid API key. Supply JAI_STUDIO_API_KEY from your secret-management workflow for SDK code, and use the CLI when a CLI command already covers the task.

Supported Usage Modes

This limited beta is primarily for developing Studio Apps locally. Production Studio App hosting runs through the Pages Rendering Service (PRS).

| Mode | Runtime | Authentication | Available modules | Supported use | | ------------------ | ------------------------------ | -------------------------------------------------------------------------------- | --------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | Direct development | Node.js 22 or later | Developer-owned Project API key | agents, members, objects, storage, workflows; Pages via CLI | Local development and explicitly controlled development automation. Arbitrary external production hosting or integration is not part of the beta support contract. | | PRS browser | Browser inside PRS | PRS session; no API key | agents, objects, storage, platform | Browser code for a PRS-hosted Studio Page. | | PRS server runtime | Node.js 22 or later inside PRS | Platform-managed Project API key injected into server-only runtime configuration | agents, members, objects, storage, workflows | Server code within PRS hosting. This is not an external integration mode. |

Only Node.js 22 or later is verified for the Node SDK and CLI. Browser support is limited to code running inside PRS.

In PRS browser mode, the Storage client preserves the public SDK shapes while adapting requests to the PRS storage transport. Folder ZIP download is not available in that mode; downloadFolder() remains available in direct and PRS server-runtime modes.

Direct Development with a Project API Key

Use a developer-owned Project API key for local server-side development or explicitly controlled development automation. The API technically accepts valid Project API keys, but that capability does not create a supported arbitrary external production-hosting contract.

Project API keys are secret-equivalent. They must never enter browser bundles, client-side JavaScript, browser-public environment variables, logs, or source control.

import { createClient } from "@japan-ai-inc/studio-sdk";

const studio = createClient({
  baseUrl: "https://api.japan-ai.co.jp",
  apiKey: "pak_your_api_key", // Get this from Studio → Settings → API Keys
});

// All modules available: agents, members, objects, storage, workflows
const agents = await studio.agents.list();
const result = await studio.agents.invoke("agent-id", { prompt: "Hello" });

Canonical Agent UUIDs are sent directly to /chat/v2; the SDK does not make a separate Agent detail request to translate them to labels. Non-UUID identifiers retain the legacy label-resolution path.

Available modules: agents, members, objects, storage, workflows

PRS Browser — No API Key

For browser code deployed as a Japan AI Studio Page, PRS provides the current session. Do not configure or read a Project API key in browser code.

import { createClient } from "@japan-ai-inc/studio-sdk";

// On PRS, baseUrl can be empty — fetch() uses relative paths.
// Auth is handled by PRS session cookies automatically.
const studio = createClient({ baseUrl: "" });

// agents.invoke() auto-detects PRS and routes through /api/agent-chat
const result = await studio.agents.invoke("550e8400-e29b-41d4-a716-446655440000", { prompt: "Hello" });

// Platform module — PRS-only features
const context = await studio.platform.waitForReady();
console.log(context.orgName, context.userName);

await studio.platform.openTask(data, "Review this");
await studio.platform.logout();

Available modules: agents (auto-routes via PRS), objects, storage, platform

PRS Server Runtime — Platform-Managed Key

PRS may inject a platform-managed Project API key into server-only runtime configuration for a hosted app. Server code may use that value with the normal Node client. Never return it from a runtime-config endpoint or forward it to browser code.

import { createClient } from "@japan-ai-inc/studio-sdk";

const apiKey = process.env["JAI_PROJECT_API_KEY"];
if (!apiKey) {
  throw new Error("Missing PRS server runtime API key");
}

const studio = createClient({
  baseUrl: "https://api.japan-ai.co.jp",
  apiKey,
});

This key lifecycle is owned by PRS hosting. It is not a credential model for arbitrary external production deployment.

Available modules (PRS server runtime): agents, members, objects, storage, workflows. The platform module is for PRS browser integration; Pages deployment remains a development/control-plane operation rather than a hosted app server-runtime capability.

Distributable Studio App Checklist

Before handing a Studio Page app to the publish/install workflow:

  • Keep source-project resource IDs out of source code and browser assets.
  • Put supported development-time Agent, Workflow, and Custom Object IDs in root .env* files under stable, descriptive server-side keys.
  • Never put Project API keys or source-project IDs in NEXT_PUBLIC_*, VITE_*, REACT_APP_*, or PUBLIC_* values.
  • Keep storage configuration as logical paths or prefixes rather than storage UUIDs.
  • Initialize browser SDK code with createClient({ baseUrl: "" }); only server-runtime code may consume a platform-managed key.
  • Activate the SDK version only after a compatible PRS runtime is deployed.

How PRS detection works:

  • The SDK checks for window.JapanAI (injected by PRS at runtime)
  • When detected, agents.invoke() routes through PRS's /api/agent-chat instead of /chat/v2
  • Other PRS-enabled modules (objects, storage) go through PRS's /api/v1/* proxy; Storage translates the PRS transport contract back to the public SDK shapes
  • Supported calls require no code changes; see the Storage note for the PRS browser downloadFolder() limitation

Limited-Beta Support

The beta audience is invited Studio App developers from existing Japan AI customers and partners. Contact your Japan AI account representative for access, support, or suspected security issues. Public package availability by itself does not expand support entitlement.

createClient(options)

| Option | Type | Required | Description | | ----------- | -------------- | -------- | --------------------------------------------------------------------------------- | | baseUrl | string | Yes | Studio API base URL (empty in PRS browser); with apiKey, HTTPS except loopback | | apiKey | string | No | Project API key for server-side modes; never used in PRS browser | | invokeUrl | string | No | Custom agent invoke URL; with apiKey, it must have the same origin as baseUrl | | timeoutMs | number | No | Default timeout (default: 30s) | | fetch | typeof fetch | No | Custom fetch implementation |

Returns a StudioClient with lazy-initialized module accessors: objects, agents, members, workflows, storage, platform.


Agents

Chat with AI agents, manage rooms, and retrieve message history.

List Agents

const { agents, totalCount, nextCursor } = await studio.agents.list({
  limit: 10,
  search: "support",
});

Get Agent

const agent = await studio.agents.get("agent-id");
// agent.id, agent.name, agent.label, agent.description, agent.enabled

Invoke (Non-Streaming)

const result = await studio.agents.invoke("550e8400-e29b-41d4-a716-446655440000", {
  prompt: "Summarize this document",
  sessionId: "room-id", // optional — continues conversation
  model: "gpt-4o", // optional
  temperature: 0.7, // optional
  systemPrompt: "...", // optional
  isEphemeral: true, // optional — don't persist to history
});

console.log(result.text); // AI response
console.log(result.sessionId); // Room ID for follow-ups
console.log(result.status); // "succeeded" | "failed"
console.log(result.references); // Source references (if any)

References use the backend-native grouped or detailed shape. Both include artifactId and filename; grouped references may include pageNumbers, sheetNames, and sourceUrl, while detailed references add index and may include a single pageNumber or sheetName.

Long-running handoff requires a persistent room: pass its roomId as sessionId, set isEphemeral: false and enableHandoffLongRunJob: true, then poll room messages until the background result appears. The SDK rejects ephemeral handoff requests.

Invoke (Streaming)

for await (const event of studio.agents.invokeStream("550e8400-e29b-41d4-a716-446655440000", { prompt: "Write a haiku" })) {
  if (event.type === "delta") {
    process.stdout.write(event.content); // Stream text chunks
  }
  if (event.type === "error") {
    console.error(event.text);
  }
}

Rooms & Message History

// Create a new chat room
const room = await studio.agents.createRoom("agent-id");
console.log(room.roomId);

// Get messages in a room
const { messages, nextCursor } = await studio.agents.getMessages("room-id", {
  limit: 20,
  cursor: "next-page-cursor",
});
// messages[0].id, .role, .content, .createdAt

Room reads are project-scoped, and a PRIVATE room is readable only by its owner. A Project API key acts as its creator, so a key shared by a multi-user app gives every caller the creator's visibility; per-user privacy is only available on the PRS page-session path.

Canvas Documents

Agents with canvas tools (html_slide decks, text and code canvases) save their output as canvas documents of the room and return only an id on the chat stream. Read them by room:

// List the room's canvas documents (metadata only, oldest first)
const { canvasArtifacts, nextCursor } = await studio.agents.listCanvasArtifacts("room-id", { limit: 20 });
const deck = canvasArtifacts.find(a => a.documentType === "hslides");
if (!deck) throw new Error("No deck in this room");

// Read one with its stored content and compare-and-set token
const artifact = await studio.agents.getCanvasArtifact("room-id", deck.id);
// artifact.content = { document_type, ...tool metadata, text }
// For html_slide decks, text is a JSON string holding the deck manifest:
const manifest = JSON.parse(artifact.content.text as string);
// manifest.slide_count, manifest.slides[].html, manifest.assets[]
// artifact.contentHash — md5 of the stored content, for change detection

room:// references inside slides[].html and assets[] entries (URI key uri, source_uri or path, depending on the last writer) are returned unresolved. Canvases created from files uploaded to the room are listed too. Requires a Public API deployment that serves the canvas read routes; older deployments return 404.

Write and Publish a Deck (PRS Browser)

A Studio Page can write an hslides deck into a room canvas and open it to anyone with its link:

// Choose the id yourself and keep it: if a write's outcome is unknown, repeat it with the same id
const canvasId = crypto.randomUUID();
await studio.agents.putCanvasArtifact(room.roomId, canvasId, {
  name: "提案書",
  content: { document_type: "hslides", text: JSON.stringify(manifest) },
});

const shared = await studio.agents.publishCanvasArtifact(room.roomId, canvasId, "ORGANIZATION");
// shared.url opens for the organization's members once they sign in

const published = await studio.agents.publishCanvasArtifact(room.roomId, canvasId, "PUBLIC");
// published.url opens without signing in

await studio.agents.unpublishCanvasArtifact(room.roomId, canvasId);
// back to the room's share scope
  • Only the signed-in user of a Page session who may write the room, in a room created through the Public API (createRoom()); a room made in the chat app is not found (404). API keys are refused (403).
  • A new id creates the canvas with the room's share scope; an existing one has its name and text replaced; an equal repeat changes nothing. Only hslides canvases (409 NOT_A_DECK).
  • content.text is at most 8 MiB in UTF-8 (413 DECK_TOO_LARGE). Inline pictures as data URLs: room:// references are refused (400 ROOM_ASSET_REFERENCE), on publishing too.
  • Publishing clears a password set in the chat app. PUBLIC needs the organization to allow public sharing (403 PUBLIC_SHARING_DISABLED); ORGANIZATION does not, and is refused while the room itself is readable without a session (409 ROOM_IS_PUBLIC), as stopping is; a new canvas in such a room is refused the same way. The scope is part of the route: a deployment without it answers 404 and changes nothing.
  • Every canvas summary, from the reads too, carries shareScope, roomShareScope, passwordProtected and url (null for a canvas without an organization or when the backend has no app origin). They describe the chat app's viewer link; they are not access control on this API.
  • Requires a Public API deployment that serves these routes; older deployments return 404 and omit the four fields.

Compare Models on One Turn (PRS Browser)

compare() asks 2–4 models for one answer each to the same user turn. The room shows one answer (the primary model's, or the lowest successful one if it fails) and keeps the others as candidates the user can switch to. Every model is billed.

let roomId: string | undefined;
let turnId = "";
const texts: string[] = [];

for await (const event of studio.agents.compare("550e8400-e29b-41d4-a716-446655440000", {
  prompt: "Summarise this plan",
  models: ["claude-opus-5-5", "gpt-6-sol"],
  primary: "claude-opus-5-5", // default: models[0]
  sessionId: roomId, // omit to start a new room
})) {
  switch (event.type) {
    case "candidates_started":
      ({ roomId, turnId } = event); // event.candidates[i].model
      break;
    case "delta":
      texts[event.candidateIndex] = (texts[event.candidateIndex] ?? "") + event.text;
      break;
    case "candidate_reset":
      texts[event.candidateIndex] = ""; // narration before a tool call, not the answer
      break;
    case "tool":
      // event.name, event.status: "running" | "done" (agents with read-only tools)
      break;
    case "candidate_done":
      // event.status: "ok" | "error" | "timeout" | "refused" | "stopped" | "pending"
      // event.metrics: { ttftMs, latencyMs, outputTokens }
      break;
    case "turn_done":
      // event.adoptedIndex undefined = not known yet; re-read with getTurns()
      break;
    case "error":
      console.error(event.code, event.text);
  }
}

// Show another candidate as the room's answer (room owner, last turn only)
await studio.agents.adoptCandidate(roomId!, turnId, 1);

// Read the room's turns with their candidates, newest page first
let before: number | undefined;
do {
  const page = await studio.agents.getTurns(roomId!, { limit: 20, before });
  // page.turns[i]: { turnId, question, answer, candidates[{ index, model, status, isAdopted, text, tools, ... }] }
  before = page.nextBefore ?? undefined;
} while (before !== undefined);
  • When the agent lets compare turns use read-only tools, each candidate also yields tool events and may yield candidate_reset. A reset drops the candidate's streamed text only, not its tool lines. A tool still running when its candidate's candidate_done arrives is finished. toolCallId can repeat across rounds: a done closes the latest running call with that id, so do not dedupe tool lines by id.
  • Available only in PRS browser mode. Outside PRS the three methods throw TurnCandidatesApiError; the Public API has no turn-candidates route yet.
  • Rooms are the signed-in viewer's own rooms in the page's project. Another project's room is reported as room_not_found.
  • adoptCandidate() and getTurns() need a page viewed by a signed-in user. On an ANONYMOUS or service-account page they are refused with 403 "A signed-in user is required".
  • Take the turn id from candidates_started.turnId. Do not derive it from a request id you send: in production the ingress replaces X-Request-ID.
  • A failed candidate carries errorCode (timeout, refused, empty_answer, context_too_long, unsupported_input, provider_unavailable, provider_rejected, internal) and a fixed English error message. Show your own text per code; provider error details are never returned.
  • Match a candidate across the stream and getTurns() by its index (candidateIndex / index), not by model name: the stream reports the requested model (plus servedModel), while getTurns() reports the model actually called, e.g. claude-sonnet-latest → claude-sonnet-5.
  • A missing adoptedIndex on turn_done means unknown, not "no candidates": the candidate write can land after the stream ends. Call getTurns() before offering an adopt.
  • compare() throws TurnCandidatesApiError (an AgentsApiError) when the request is refused before streaming starts; code is the agent-service code when PRS relays one (such as invalid_candidates), else BAD_REQUEST. A refusal by the agent service after PRS opens the stream, such as a model the Agent does not allow or a busy room, arrives as an error event with the same code.
  • adoptCandidate() and getTurns() refusals throw TurnCandidatesApiError with code: BAD_REQUEST for PRS-local checks, otherwise the agent-service code, such as not_last_turn, generation_in_progress, room_busy, memory_compacted, room_frozen, turn_deleted, candidate_not_ok, candidate_not_found, turn_not_found, room_not_found, not_enabled, forbidden or adopt_failed.
  • invokeStream() is unchanged.

What Turns and Rooms Cost (PRS Browser)

In a Page, invoke() results and invokeStream() deltas carry requestId: the turn's request id in the credit ledger. Keep the ids an action makes, then read what they cost, or read what a whole room has cost:

const ids: string[] = [];
for await (const event of studio.agents.invokeStream(agentId, { prompt })) {
  if (event.type === "delta" && event.requestId && !ids.includes(event.requestId)) {
    ids.push(event.requestId);
  }
}

// Each larger total as the ledger fills (read 5 s, 30 s, 2 min, 5 min after).
for await (const credits of studio.agents.followTurnCredits(ids)) {
  showCost(credits);
}

// Once: the turns' total so far, and a room's running total.
const { credits } = await studio.agents.getTurnCredits(ids);
const room = await studio.agents.getRoomCredits(roomId);
  • Turn reads count only the signed-in user's own turns in this project from the last 24 hours, 1-200 ids per call: the turns' own model and tool calls and the workflows they called. Keep ids per action, not for the page's lifetime.
  • A room's total counts every member's turns, the workflows they called and background coworkers, and is readable by whoever may read the room. It needs no ids, so it survives a reload.
  • The ledger holds a turn seconds after it ends (minutes on a retry): a read right after it may be 0. followTurnCredits() and followRoomCredits() read again, yield only larger totals, and end quietly where the cost cannot be read here (403, 404) or when signal aborts. Following no ids yields nothing.
  • These calls need a page viewed by a signed-in user; anywhere else they throw AgentsApiError.

PRS Behavior

When running inside the Pages Rendering Service (PRS), invoke() and invokeStream() automatically route through /api/agent-chat instead of /chat/v2. The SDK detects PRS via window.JapanAI and classifies the first argument after trimming it:

  • A canonical 8-4-4-4-12 hexadecimal UUID (case-insensitive, with no version/variant restriction) is normalized to lowercase and sent as agent_id. PRS resolves and authorizes that ID from the authenticated Page's release-managed runtime metadata. An ID not declared by that Page is rejected; it does not fall back to label routing.
  • Any other non-empty string is sent as the legacy agent_label. For example, "agent-id" is a label, not a stable Agent ID.
  • An empty or whitespace-only string sends neither field, allowing PRS to use its configured fallback Agent.

SDK-generated requests never contain both identity fields. A legacy Agent whose label is itself a canonical UUID must use the low-level studio.platform.chat({ agentLabel }) API to force label routing; that path does not use release-managed Agent ID resolution or its version/runtime pinning. Outside PRS, the existing Agent lookup and invocation path remains unchanged.

The compatible PRS agent_id contract must be deployed before an SDK version using UUID routing is released or activated. An older PRS may ignore agent_id and invoke its fallback Agent instead.

In PRS, storageFileIds attaches Studio Storage files by fileId instead of sending their bytes from the browser. PRS checks each file with the Page user's storage permissions and the Agent reads the stored file itself, so replacing or deleting it later changes what later turns of the room see. A call takes at most 20 IDs, and it can be combined with files. Outside PRS the SDK rejects storageFileIds.

const { files } = await studio.storage.upload(images, { path: "/inputs" });
const result = await studio.agents.invoke(agentId, {
  prompt: "Describe these slides",
  storageFileIds: files.map(file => file.fileId),
});

Objects

Manage custom object schemas and their records.

Object Definitions

// List all objects
const { data } = await studio.objects.list();

// Create an object
const obj = await studio.objects.create({ objectName: "contacts" });

// Get / Update / Delete
const obj = await studio.objects.get("objectId");
await studio.objects.update("objectId", { displayName: "Contacts" });
await studio.objects.delete("objectId");

Records (via define<T>())

define<T>() returns a typed handle for record operations on a specific object.

type Contact = { name: string; email: string; company?: string };
const contacts = studio.objects.define<Contact>("contacts");

// CRUD
const record = await contacts.create({ name: "Acme", email: "[email protected]" });
const fetched = await contacts.get("record-id");
const updated = await contacts.update("record-id", {
  name: "Acme",
  email: "[email protected]",
  company: "Acme Inc",
});
await contacts.delete("record-id");

// List with pagination
const page = await contacts.list({ limit: 20, cursor: "next-cursor" });
// page.data        → RecordItem<Contact>[]
// page.hasNextPage → boolean
// page.cursor      → string | null

// Search with filters
const results = await contacts.search({
  filters: [{ field: "name", operator: "contains", value: "Acme" }],
  sort: [{ field: "createdAt", direction: "desc" }],
  limit: 50,
});

// Bulk create (up to 1000 records)
const created = await contacts.bulkCreate([
  { name: "Alice", email: "[email protected]" },
  { name: "Bob", email: "[email protected]" },
]);

Record updates use full-replacement semantics. Pass the complete record data; ordinary fields omitted from update() are removed by the server.

Every record carries a revision that increments on each write. To make an update or delete compare-and-swap (fail with 409 Revision conflict when the record changed since you read it), pass that revision back:

const record = await contacts.get("record-id");
if (record.revision === undefined) {
  throw new Error("Server does not return record revisions");
}

// Guarded update — 409 if the record changed since the get above.
// `update` returns the fresh record; its revision is now record.revision + 1.
await contacts.update("record-id", data, undefined, {
  expectedRevision: record.revision,
});

// Guarded delete — as a separate example, using the revision read above
// (reusing a revision from before a successful update would 409, because
// every write increments it):
await contacts.delete("record-id", undefined, {
  expectedRevision: record.revision,
});

Omit expectedRevision for last-write-wins (update) / unconditional delete.

Fields

const contacts = studio.objects.define<Contact>("contacts");

// List fields
const fields = await contacts.listFields();

// Create a field
const field = await contacts.createField({
  fieldName: "phone",
  displayName: "Phone Number",
  dataType: "phone",
  fieldNum: 5, // positive, 1-indexed field position
});

// Update / Delete
await contacts.updateField("fieldId", { displayName: "Mobile" });
await contacts.deleteField("fieldId");

Field validation rules

Use a version of the SDK that exports FieldValidationRule and a compatible backend before using these examples. Older Public API deployments reject numeric rule IDs; deploy the compatible Public API before releasing the SDK. Do not substitute a UUID string to work around that error.

import type { FieldValidationRule } from "@japan-ai-inc/studio-sdk";

// Resolve the object and field from the intended project's list/get results.
const object = studio.objects.define("<object-id>");
const fields = await object.listFields();
const indexField = fields.find(field => field.fieldName === "chunk_index");
if (!indexField) throw new Error("Target field not found");

const rules: FieldValidationRule[] = [{ id: 10 }]; // IntegerOnly
// Replaces the complete rule list; use only when this is the intended list.
await object.updateField(indexField.fieldId, { validationRules: rules });

validationRules uses numeric IDs and optional config, not UUIDs. Rule configuration JSON is opaque: preserve keys and values, including nested objects in AllowedValues (id: 4). The SDK preserves this content in direct Public API and PRS responses.

Field PATCH semantics:

  • Omit validationRules to preserve the existing rule list.
  • Send validationRules: [] only when intentionally clearing all rules.
  • Send a nonempty array to replace the entire list, not merge individual rules. Read the current list first and include every rule that should remain.

DateRange (id: 6) bounds must be timestamp strings with seconds and a timezone:

const dateRules: FieldValidationRule[] = [
  {
    id: 6,
    config: {
      min: "2026-01-01T00:00:00Z",
      max: "2026-12-31T23:59:59.999+09:00",
    },
  },
];

Do not supply date-only strings, numeric timestamps, invalid calendar dates, or timestamps without seconds or a timezone. Omit min or max only when that side should be unbounded. A supplied DateRange replaces the previous rule: omitting a bound does not preserve the old bound. Omitting both bounds makes the range unbounded.

The compatible Public API rejects invalid supplied DateRange bounds. The SDK itself does not validate them at runtime, and direct PRS calls do not pass through that Public API guard. Do not assume PRS rejects malformed bounds; use the format above on both paths.

Adding or tightening a constraint can trigger validation of existing records. An HTTP success response alone does not mean the new constraint is active. Re-read fields with listFields() until the intended validationRules appear; where the response exposes constraintValidationState, require READY and no validation error. Stop and inspect failures or a validation timeout instead of blindly replaying the write. Never delete and recreate a field to restore its rules, or use a guessed parent object ID to make a write seem harmless.

Search Filter Operators

| Operator | Description | | ------------------------- | ----------------------- | | eq | Equal | | ne | Not equal | | gt / gte | Greater than / or equal | | lt / lte | Less than / or equal | | contains | String contains | | startswith / endswith | String prefix/suffix | | in / notin | Value in/not in array | | isnull / isnotnull | Null check | | exists | Field exists | | regex | Regex match | | between | Range check | | jsoncontains | JSON field contains |


Storage

Upload, download, and manage files and folders.

List Files

const { files, folders, totalCount, nextCursor } = await studio.storage.list({
  path: "/documents",
  recursive: true,
  limit: 50,
  orderBy: "createdAt",
  orderDirection: "desc",
});

Upload Files

// Single file
const result = await studio.storage.upload(file, { path: "/uploads" });

// Multiple files (up to 20)
const result = await studio.storage.upload([file1, file2, file3], {
  path: "/uploads",
});
// result.files → StorageFile[]

// Atomically replace files with matching names in the target path.
// Existing rows keep the same fileId; non-conflicting files are created.
const replaced = await studio.storage.upload(file, {
  path: "/uploads",
  conflictResolution: "replace",
});

Download

// Download file content
const response = await studio.storage.download("file-id");
const blob = await response.blob();

// Get a signed download URL
const { downloadUrl, expiresAt } = await studio.storage.getDownloadUrl("file-id", { expiresInMinutes: 60 });

// Download an entire folder as a zip (direct and PRS server-runtime modes)
const response = await studio.storage.downloadFolder("folder-id", {
  maxSizeMb: 100,
});

downloadFolder() is not available in PRS browser mode.

File Metadata & Deletion

const metadata = await studio.storage.getMetadata("file-id");
// metadata.fileId, .fileName, .path, .mimeType, .sizeBytes

await studio.storage.deleteFile("file-id");
// { fileId, message }

Folders

// Create a folder
const folder = await studio.storage.createFolder({
  folderName: "reports",
  path: "/documents",
});

// List folder contents
const contents = await studio.storage.listFolderContents("folder-id", {
  recursive: true,
  limit: 100,
});

// Delete a folder
const result = await studio.storage.deleteFolder("folder-id");
// result.deletedCount — number of items deleted

Resumable Upload (Large Files)

For large files, use the two-step prepare → finalize flow:

// 1. Prepare — get a signed upload URL
const prepared = await studio.storage.prepareUpload({
  fileName: "large-video.mp4",
  fileSize: 500_000_000,
  mimeType: "video/mp4",
  path: "/media",
});

// 2. Upload directly to storage (using the signed URL)
await fetch(prepared.uploadUrl, {
  method: "PUT",
  headers: prepared.requiredHeaders,
  body: fileContent,
});

// 3. Finalize — register the file in Studio
const file = await studio.storage.finalizeUpload({
  storageId: prepared.storageId,
  fileName: "large-video.mp4",
  fileSize: 500_000_000,
  mimeType: "video/mp4",
});

To atomically replace an existing file with the same name and path, pass conflictResolution: "replace" to prepareUpload() and send the returned replaceIntent back unchanged when finalizing:

const prepared = await studio.storage.prepareUpload({
  fileName: "large-video.mp4",
  fileSize: 500_000_000,
  mimeType: "video/mp4",
  path: "/media",
  conflictResolution: "replace",
});

await fetch(prepared.uploadUrl, {
  method: "PUT",
  headers: prepared.requiredHeaders,
  body: fileContent,
});

const file = await studio.storage.finalizeUpload({
  storageId: prepared.storageId,
  replaceIntent: prepared.replaceIntent,
  fileName: "large-video.mp4",
  fileSize: 500_000_000,
  mimeType: "video/mp4",
  path: "/media",
});

Workflows

Create, publish, trigger, and monitor workflows.

List Workflows

const { workflows, totalCount, nextCursor } = await studio.workflows.list({
  limit: 10,
});

By default only workflows with a published snapshot are returned. Use status to include drafts:

// Draft-only workflows (no published snapshot)
const drafts = await studio.workflows.list({ status: "draft" });

// Everything — each row carries isPublished
const all = await studio.workflows.list({ status: "all" });
for (const wf of all.workflows) {
  console.log(wf.name, wf.isPublished);
}

Get Workflow

const workflow = await studio.workflows.get("workflow-id");
// workflow.id, .name, .description, .enabled, .isPublished
// workflow.definition: { graph, features, source: "draft" | "v1" } | null

definition is the stored draft (or the published v1 snapshot for a legacy workflow without a draft). Webhook secrets inside graph are redacted to placeholders, so a definition read back can be edited and passed to saveDraft() or publish() as-is. definition.draftUpdatedAt is an opaque concurrency token: pass it back as expectedDraftUpdatedAt and the write is rejected with 409 if someone else changed the draft in between.

Create a Workflow

Requires the workflow:write scope on the Project API key. The new workflow starts with an empty draft and is not published.

const workflow = await studio.workflows.create({
  name: "Invoice intake",
  description: "Routes inbound invoices", // optional
  enabled: true, // optional, defaults to true
});
// workflow.isPublished === false

Update Workflow Metadata

Requires the workflow:write scope. At least one field must be provided. Setting enabled: false also deactivates the workflow's email and storage triggers.

const updated = await studio.workflows.update(workflow.id, {
  name: "Invoice intake v2",
  description: "Routes inbound invoices",
  enabled: false,
});

Save a Draft

Requires the workflow:write scope. Stores the definition as the workflow's draft without publishing it. Read the current definition first and pass its token back so concurrent edits cannot silently overwrite each other.

const current = await studio.workflows.get(workflow.id);
if (!current.definition) throw new Error("Workflow has no definition yet");

const saved = await studio.workflows.saveDraft(workflow.id, {
  graph: editGraph(current.definition.graph), // your edit of the read-back graph
  features: { sandbox_provider: "e2b" }, // optional; "e2b" | "k8s"
  expectedDraftUpdatedAt: current.definition.draftUpdatedAt, // optional; 409 if the draft moved
});
// saved.snapshotId, saved.draftUpdatedAt

Every successful save spends the token you passed in and returns the replacement in draftUpdatedAt; use that one for the next saveDraft() or publish() instead of re-reading.

Publish a Workflow

Requires the workflow:write scope. Publishing stores the supplied graph as the workflow's live v1 snapshot and registers its triggers. Triggers whose external connection could not be registered are reported in unregisteredTriggers; the publish itself still succeeds. Start from a definition read back with get() (or the draft you just saved) rather than assembling a graph by hand.

const current = await studio.workflows.get(workflow.id);
if (!current.definition) throw new Error("Workflow has no definition yet");

const { snapshot, unregisteredTriggers } = await studio.workflows.publish(workflow.id, {
  graph: editGraph(current.definition.graph),
  features: current.definition.features,
  expectedDraftUpdatedAt: current.definition.draftUpdatedAt, // or saved.draftUpdatedAt after saveDraft()
});
// snapshot.id, snapshot.version ("v1")
for (const trigger of unregisteredTriggers) {
  console.warn(trigger.type, trigger.nodeId, trigger.reason);
}

The graph body is validated server-side. A 409 means either the workflow is release-managed (installed from an app) or the draft changed since the token was read.

Delete a Workflow

Requires the workflow:delete scope. Deleting also deactivates the workflow's trigger tasks.

await studio.workflows.delete(workflow.id);

CLI

jai-studio workflows create --name "Invoice intake"
jai-studio workflows get <workflow-id> | jq .definition > ./definition.json
jai-studio workflows update <workflow-id> --name "Invoice intake v2" --disable
jai-studio workflows save-draft <workflow-id> --file ./definition.json
jai-studio workflows publish <workflow-id> --file ./definition.json
jai-studio workflows delete <workflow-id>

--file takes the definition object exactly as workflows get prints it: graph (with nodes and edges) and, when present, features are sent, draftUpdatedAt is forwarded as the expectedDraftUpdatedAt precondition, and other keys such as source are ignored. A present but malformed token is an error rather than a silent fallback. Edit that file rather than writing a graph from scratch.

save-draft prints the replacement draftUpdatedAt; a later publish from the same file must carry that token (update the file or re-run get). To go live directly, skip save-draft and publish the edited get output.

Run a Workflow

const { runId, status } = await studio.workflows.run("workflow-id", {
  inputs: { documentUrl: "https://..." },
});

// Fetch outputs and keep polling if the returned status is still RUNNING
const result = await studio.workflows.getRunResult(runId);
// status: "RUNNING" | "SUCCEEDED" | "FAILED" |
//         "PARTIAL_SUCCEEDED" | "STOPPED"
// result.outputs: Record<string, unknown>

run() starts an execution but is not currently a guaranteed fire-and-forget transport: the runtime may keep the request open until the workflow reaches a terminal state. If the API returns a WORKFLOW_TIMEOUT 408 error after execution may have started, WorkflowsApiError.responseBody includes the recoverable runId; pass it to getRunResult() to continue checking that execution.

Run and Wait

Blocks until the workflow completes (default timeout: 60 seconds):

const result = await studio.workflows.runAndWait("workflow-id", {
  inputs: { query: "market trends" },
  timeoutMs: 120_000,
});

if (result.status === "SUCCEEDED" || result.status === "PARTIAL_SUCCEEDED") {
  console.log(result.outputs);
} else {
  console.error(result.error);
}

timeoutMs is an end-to-end deadline. Supported range is 1,000–270,000 ms; the SDK reserves the remaining 30 seconds of the PRS five-minute request budget for authentication, proxying, and response delivery.


Members

List the members of the current project.

Direct development may use a Project API key. PRS-hosted page apps require PRS to allow-list GET /api/v1/project-members before this module can be used in browser mode without an API key.

GET /api/v1/project-members has three independent prerequisites, and a missing one answers 403 with a message that names it:

| Prerequisite | Message when missing | | -------------------------------------------------- | --------------------------------------------------------- | | member:read on the key | API key missing required scope(s): member:read | | EnableProjectMemberPublicApi on the organization | Project member API is not enabled for this organization | | IAM permission for the caller | a 403 from the IAM check |

A plain Invalid API key means the key itself did not resolve — it was revoked, belongs to another environment, or is the masked value printed by config show. It does not indicate a scope or flag problem.

List Members

const { members, totalCount, nextCursor } = await studio.members.list({
  search: "ada",
  limit: 50,
  cursor: "next-page-cursor",
});
// members[0].id, .principalId, .principalType, .name, .email, .avatarUrl, .createdAt

CLI

jai-studio members list
jai-studio members list --search ada --limit 50
jai-studio members list --cursor <next-cursor>

totalCount is the project total, so a membership count needs one call, not a full pagination pass. Member records are personal data: keep them out of logs, fixtures, and generated examples.


Platform (PRS) — Beta

Beta: PRS is an internal runtime. This module's API may change between minor versions. Pin to an exact SDK version if you depend on it.

Browser-only module for apps running inside Japan AI's Pages Rendering Service. This module does not require baseUrl or apiKey — PRS injects authentication automatically.

Detect PRS

if (studio.platform.isAvailable()) {
  const context = studio.platform.getContext();
  console.log(context.orgName, context.userName);
}

Wait for PRS Ready

PRS injects window.JapanAI asynchronously. Use waitForReady() to wait:

const context = await studio.platform.waitForReady(5000); // timeout in ms
if (context) {
  // PRS is ready
  console.log(context.userId, context.projectId, context.pageId);
} else {
  // Not running on PRS, or timed out
}

Platform Context

getContext() returns:

type PlatformContext = {
  userId: string;
  email: string;
  userName: string;
  memberRole: string;
  orgId: string;
  orgName: string;
  projectId: string;
  pageId: string;
  customObjects: Record<string, string>; // name → objectId
};

Open Task

Delegate data to an agent task in the Studio UI:

const result = await studio.platform.openTask(
  [{ name: "John", email: "[email protected]" }], // data
  "Please review this contact", // user prompt
  {
    agentLabel: "review-agent",
    isTemporary: false,
    isEphemeral: false,
  }
);
console.log(result.roomId);

Logout

Revoke PRS sessions for the current Page session subject from a user-facing logout button:

await studio.platform.logout();

This revokes every PRS session that shares the current Page session subject and Page scope, clears this browser's current Page cookie, and reloads the Page document. Other Pages and the Japan AI account are unaffected. On externally authenticated Pages, that subject is the external IdP subject, so the reload re-enters that Page's external login flow. Do not manually clear PRS cookies or construct /__internal/session/logout URLs.

Open App Settings

When a Page is an installed App's only entry, users cannot reach the App settings through the Studio navigation. Open the settings dialog over the Page instead, from the Page's own settings button or from an error that asks the user to connect a connector:

await studio.platform.openAppSettings("connectors");

The optional tab is "basic", "members", "iamRoles", "connectors", "secrets", or "dataIntegration". The dialog shows only the tabs the viewer can use, so it may open on another one. Members who do not manage the App still use it to connect their personal credentials, so show the entry to every member rather than hiding it by memberRole.

The promise rejects with code BAD_REQUEST for an unknown tab, and with UNSUPPORTED when the Page runs in a Studio project rather than an installed App, the organization has not enabled the feature, or the Page is opened directly instead of embedded. Hosts that do not handle the request, such as public share links, leave it unanswered, so it rejects with TIMEOUT after 30 seconds. Catch these and keep the rest of the Page working.

Chat (Low-Level PRS Proxy)

Direct access to PRS's /api/agent-chat endpoint (prefer studio.agents.invoke() instead — it auto-routes on PRS):

Use agentLabel here when you intentionally need the legacy label path, including for a label that is itself shaped like a canonical UUID. Label-only calls do not use release-managed Agent ID resolution or its version/runtime pinning.

const response = await studio.platform.chat({
  messages: [{ role: "user", content: "Hello" }],
  agentLabel: "support-agent",
  model: "gpt-4o",
  temperature: 0.7,
});
console.log(response.content, response.roomId);

Error Handling

Each module throws its own error class, all extending SdkApiError:

import { ObjectsApiError, AgentsApiError, StorageApiError, WorkflowsApiError } from "@japan-ai-inc/studio-sdk";

try {
  await studio.agents.invoke("agent-id", { prompt: "hello" });
} catch (err) {
  if (err instanceof AgentsApiError) {
    console.error(err.statusCode); // 401, 404, 500, etc.
    console.error(err.message); // Human-readable error
    console.error(err.responseBody); // Raw error response
    console.error(err.code); // The route's refusal code, e.g. "PUBLIC_SHARING_DISABLED", when sent
  }
}

Timeouts & Cancellation

Default timeout is 30 seconds. Override per-request or globally:

// Global timeout
const studio = createClient({ baseUrl: "...", apiKey: "...", timeoutMs: 60000 });

// Per-request timeout
await contacts.list({ limit: 10 }, { timeoutMs: 5000 });

// Cancel with AbortController
const controller = new AbortController();
const promise = contacts.search({ filters: [...] }, { signal: controller.signal });
controller.abort();

Agent invoke has longer defaults: 5 minutes for invoke(), 10 minutes for invokeStream().


Versioning & Stability

This package follows Semantic Versioning.

Pre-1.0 policy (0.x.y): The SDK is in active development. Minor version bumps (0.2.0, 0.3.0) may contain breaking changes to method signatures, error shapes, or module structure. Patch versions within the same minor are backwards-compatible bug fixes and security patches.

Recommendation: Pin to an exact version in package.json until 1.0:

"@japan-ai-inc/studio-sdk": "0.7.0"

What counts as a breaking change:

  • Removing or renaming an exported function, class, or type
  • Changing the signature of a public method (required params, return type)
  • Changing error class hierarchy or statusCode semantics
  • Removing a CLI command or changing its required arguments

What does NOT count as breaking:

  • Adding new optional parameters, methods, or exports
  • Improving error messages
  • Bug fixes to match documented behavior

Release history is included in the published package as CHANGELOG.md. After a project-local install, read it at node_modules/@japan-ai-inc/studio-sdk/CHANGELOG.md.