@schift-io/sdk
v0.13.0
Published
Workspace SDK for embeddings, search, RAG chat, and workflows
Downloads
1,453
Maintainers
Readme
@schift-io/sdk
TypeScript SDK for bucket upload, vector search, chat, and workflows.
Install
npm install @schift-io/sdkQuick Start
import { WorkspaceClient } from "@schift-io/sdk";
const client = new WorkspaceClient({
apiKey: "sch_your_api_key",
baseUrl: process.env.SCHIFT_API_URL!,
});
// Create or reuse a bucket, upload a document, then search it.
// All bucket methods accept a name or ID — no need to track UUIDs.
await client.createBucket({ name: "company-docs" });
const file = new File([await readFile("manual.pdf")], "manual.pdf", {
type: "application/pdf",
});
await client.db.upload("company-docs", { files: [file] });
const jobs = await client.listJobs({ bucket: "company-docs", limit: 5 });
const search = await client.bucketSearch("company-docs", {
query: "refund policy",
topK: 5,
});
console.log(jobs[0]?.status ?? "queued");
console.log(search.context, search.citations);Use search({ bucket: ... }) for an answer-ready context block with citations, retrieve(bucketId, { query }) for raw scored chunks, POST /v1/chat for bucket-backed RAG chat with sources, and POST /v1/chat/completions for OpenAI-compatible LLM routing without bucket context.
API Key
Create or copy your API key from your workspace dashboard.
Configure the hosted API origin with baseUrl, SCHIFT_API_URL, or
SCHIFT_BASE_URL. The SDK does not assume a production origin when none is
configured.
const client = new WorkspaceClient({
apiKey: process.env.SCHIFT_API_KEY!,
baseUrl: process.env.SCHIFT_API_URL!,
});Local Auth Readiness
/v1/auth/me validates a user JWT, not a sch_* API key. Smoke tests should
use fixed accounts from .env/.env.local; do not generate timestamped signup
emails against production.
import { WorkspaceClient } from "@schift-io/sdk";
const auth = WorkspaceClient.auth({ baseUrl: "http://127.0.0.1:8011" });
await auth.health();
const login = await auth.login({
email: process.env.SCHIFT_SMOKE_SYSTEM_EMAIL!,
password: process.env.SCHIFT_SMOKE_SYSTEM_PASSWORD!,
});
const me = await auth.me(login.token!);
console.log(me.user.email, me.orgs[0]?.name);With a local API already running:
SCHIFT_API_URL=http://127.0.0.1:8011 \
[email protected] \
SCHIFT_SMOKE_SYSTEM_PASSWORD=... \
npm run smoke:authFeatures
Embeddings
// Single text
const resp = await client.embed({
text: "Search query",
model: "openai/text-embedding-3-small", // optional
dimensions: 1024, // optional
});
// resp: { embedding: number[], model: string, dimensions: number, usage: { tokens: number } }
// Batch (up to 2048 texts)
const batch = await client.embedBatch({
texts: ["doc 1", "doc 2", "doc 3"],
model: "gemini/text-embedding-004",
});
// batch: { embeddings: number[][], model: string, dimensions: number, usage: { tokens, count } }Search (answer-ready)
const response = await client.search({
query: "How does projection work?",
bucket: "my-docs",
topK: 10,
});
// response: { status, operational_status, bucket_id, query,
// context: string, citations: [{ index, title?, page?, ... }],
// warnings: [{ code, message, severity }] }Retrieve (raw chunks)
const retrieved = await client.retrieve("my-docs-bucket-id", {
query: "refund policy",
topK: 8,
options: { rerank: { enabled: true } },
});
// retrieved.results: Array<{ chunk_id, text, score, document_id?, metadata?, ... }>Web Search
// Hosted web search
const results = await client.webSearch("latest AI regulations 2026", 5);
results.forEach((r) => {
console.log(r.title, r.url);
});// BYOK provider for direct web search
import { WebSearch } from "@schift-io/sdk";
const webSearch = new WebSearch({
provider: "tavily",
providerApiKey: process.env.TAVILY_API_KEY!,
maxResults: 5,
});
const fresh = await webSearch.search("Schift framework launch updates");Tool calling helpers created from client.tools include workspace_web_search when web search is enabled, so OpenAI/Claude/Vercel AI SDK integrations can call live web search without extra wiring.
PII Masking
Mask Korean PII before sending text to an LLM, vector store, or workflow step.
Use types when you want the masking contract to be visible and selectable.
const piiTypes = [
"resident_id",
"alien_registration",
"passport",
"driver_license",
"address",
"phone",
"bank_account",
];
const result = await client.redactPii({
text: "주민등록번호 850205-1234567, 연락처 010-1234-5678",
returnMode: "both",
types: piiTypes,
});
const masked = await client.mask("계좌 123-45-678901", {
types: ["bank_account"],
});The default token format returns tokens such as [PII_PHONE_1] and a
reverse_map so the caller can restore the original values after the workflow
step. Keep that map only in your temporary restore path; Schift does not
persist or gateway-cache it for pii_type_index requests. Set
tokenFormat: "label_index" only when you need legacy tokens such as [PHONE_1].
Send only masked into the LLM, agent, vector store, or workflow step. Never
include reverse_map in the AI payload; use it only after the AI result returns
if your app needs to restore values.
const restored = await client.restorePii({
text: "고객 연락처 [PII_PHONE_1]로 안내하세요.",
reverseMap: result.reverse_map!,
});restorePii() is a stateless API call. It requires an API key and does not
persist or cache the map.
BYOK (Bring Your Own LLM Key)
Register your own OpenAI / Google / Anthropic key so /v1/chat and /v1/chat/completions call the provider directly instead of consuming the hosted platform shared LLM quota. Supported providers: "openai", "google", "anthropic".
// Register a key
await client.providers.set("google", {
api_key: process.env.GOOGLE_API_KEY!,
// endpoint_url: "https://custom-proxy.example.com", // optional
});
// Check whether a provider is configured (api_key is never returned)
const status = await client.providers.get("openai");
// { provider: "openai", configured: true | false, endpoint_url: string | null }Rotation: a stored BYOK record shadows any server-side env var or secret for that provider. To rotate, call set() again with the new key — changing env vars alone has no effect on orgs with a BYOK record.
Agent SDK Compatibility
The SDK sits underneath the agent framework. The integration point is always the same:
- let the agent call a workspace search tool
- run retrieval against workspace buckets
- return grounded chunks back to the model
That means you can keep your preferred agent SDK and swap only the retrieval layer.
Workflow v2 SDK Adapters
Run complete Workflow v2 graphs through the local SDK runtime. The YAML stays a contract in the user's process; it is not sent to the hosted platform unless you explicitly save/publish it through the workflow API:
import { WorkspaceClient } from "@schift-io/sdk";
const client = new WorkspaceClient({
apiKey: process.env.SCHIFT_API_KEY!,
baseUrl: process.env.SCHIFT_API_URL!,
});
const wf = client.workflow({ yaml });
const run = await wf.run({
query: "계약서 리스크 봐줘",
});
// For local nodes that need hosted APIs, pass the client explicitly:
await wf.run({ inputs: { query: "계약서 리스크 봐줘" }, client });
// Webhook/metadata side effects stay caller-owned through middleware.
await wf.run({
inputs: { webhooks: { "teacher-request": { caseId: "case_1" } } },
middleware: {
receiveWebhook: (event) => event.payload,
writeMetadata: async (entry) => {
await localDb.metadata.put(entry.namespace, entry.key, entry.value);
return { stored: true };
},
deliverWebhook: async (event) => {
await appServer.deliver(event.webhook, event.payload);
return { delivered: true };
},
requestHttp: (request) => appServer.fetch(request),
readSecret: (request) => localSecrets.get(request.secret),
requestApproval: (request) => approvals.enqueue(request),
requestForm: (request) => forms.enqueue(request),
wait: (request) => scheduler.defer(request),
runSubworkflow: (request) => workflowRegistry.run(request.workflowRef, request.subworkflowInputs),
},
});
for await (const event of wf.stream({ query: "계약서 리스크 봐줘" })) {
if (event.type === "block.completed") console.log(event.blockId);
if (event.type === "workflow.completed") console.log(event.run.outputs);
}Framework adapters are intentionally narrower. asVercelAI() projects one
selected Workflow v2 llm_generate block into Vercel AI SDK call options. It
does not execute upstream retrieval, transforms, conditions, or the full graph.
Use the local runtime with workflow.run() / workflow.stream() for graph
execution. Use client.workflowsV2.run(workflowId, options) only for
intentionally hosted, published Workflow v2 definitions:
const hosted = await client.workflowsV2.run("wf_v2_123", {
inputs: { invoice: "T-001" },
mode: "live",
approvals: { review_issue: true },
mockBindings: { tax_provider: { status: "mocked" } },
});import { generateText } from "ai";
import { WorkspaceClient } from "@schift-io/sdk";
import { asVercelAI } from "@schift-io/workflow-vercel-ai";
const client = new WorkspaceClient({
apiKey: process.env.SCHIFT_API_KEY!,
baseUrl: process.env.SCHIFT_API_URL!,
});
const wf = client.workflow({ yaml });
const result = await generateText(
await asVercelAI(wf, {
mode: "generateText",
entry: "answer",
}),
);Structured output schemas can live in the Workflow v2 YAML block config as
response_schema / output_schema / schema. Vercel adapters pass that schema
through as schema for generateObject/streamObject, while Google Gen AI
adapters map it to config.responseMimeType = "application/json" and
config.responseSchema.
import { GoogleGenAI } from "@google/genai";
import { WorkspaceClient } from "@schift-io/sdk";
import { asGoogleGenAI } from "@schift-io/workflow-google-genai";
const ai = new GoogleGenAI({ apiKey: process.env.GEMINI_API_KEY! });
const client = new WorkspaceClient({
apiKey: process.env.SCHIFT_API_KEY!,
baseUrl: process.env.SCHIFT_API_URL!,
});
const wf = client.workflow({ yaml });
const response = await ai.models.generateContent(
await asGoogleGenAI(wf, { entry: "answer" }),
);import { asLangGraph } from "@schift-io/workflow-langgraph";
const graph = await asLangGraph(wf, {
state: { messages: [] },
});
const result = await graph.invoke({
messages: [{ role: "user", content: "계약서 리스크 봐줘" }],
});Use asDify() when you need a Dify-compatible workflow export graph from a
canonical Workflow v2 artifact:
import { asDify } from "@schift-io/workflow-dify";
const difyExport = await asDify(wf);The adapter packages declare framework SDKs as peer dependencies and do not add
Vercel AI SDK, Google Gen AI, LangGraph, or Dify-specific projection code to the
core @schift-io/sdk bundle.
Google Gen AI SDK
import { GoogleGenAI } from "@google/genai";
import { WorkspaceClient } from "@schift-io/sdk";
const ai = new GoogleGenAI({ apiKey: process.env.GEMINI_API_KEY });
const client = new WorkspaceClient({
apiKey: process.env.SCHIFT_API_KEY!,
baseUrl: process.env.SCHIFT_API_URL!,
});
const firstTurn = await ai.models.generateContent({
model: "gemini-2.5-flash",
contents: "What changed in the latest billing policy?",
config: {
tools: client.tools.googleGenAI(),
},
});
const functionCall = firstTurn.functionCalls?.[0];
if (functionCall) {
const functionResponsePart = await client.tools.googleFunctionResponse({
name: functionCall.name,
args: functionCall.args ?? {},
});
const secondTurn = await ai.models.generateContent({
model: "gemini-2.5-flash",
contents: [
{ role: "user", parts: [{ text: "What changed in the latest billing policy?" }] },
{ role: "model", parts: [{ functionCall }] },
{ role: "user", parts: [functionResponsePart] },
],
});
console.log(secondTurn.text);
}Vercel AI SDK
import { generateText } from "ai";
import { openai } from "@ai-sdk/openai";
import { WorkspaceClient } from "@schift-io/sdk";
const client = new WorkspaceClient({
apiKey: process.env.SCHIFT_API_KEY!,
baseUrl: process.env.SCHIFT_API_URL!,
});
const result = await generateText({
model: openai("gpt-4o-mini"),
prompt: "What changed in the latest billing policy?",
tools: client.tools.vercelAI(),
maxSteps: 5,
});
console.log(result.text);Mastra
If you are using Mastra, wrap client.search() in a Mastra tool and keep the rest of the agent stack unchanged.
import { Agent } from "@mastra/core/agent";
import { createTool } from "@mastra/core/tools";
import { openai } from "@ai-sdk/openai";
import { z } from "zod";
import { WorkspaceClient } from "@schift-io/sdk";
const client = new WorkspaceClient({
apiKey: process.env.SCHIFT_API_KEY!,
baseUrl: process.env.SCHIFT_API_URL!,
});
const workspaceSearchTool = createTool({
id: "workspace-search",
description: "Retrieve context from a workspace bucket.",
inputSchema: z.object({
bucket: z.string(),
query: z.string(),
topK: z.number().int().min(1).max(10).default(5),
}),
execute: async ({ context }) => ({
search: await client.search({
bucket: context.bucket,
query: context.query,
topK: context.topK,
}),
}),
});
const agent = new Agent({
name: "docs-agent",
instructions: "Use workspace-search before answering document questions.",
model: openai("gpt-4o-mini"),
tools: { workspaceSearchTool },
});Examples:
Buckets
// List all buckets
const buckets = await client.listBuckets();
await client.db.upload("company-docs", {
files: [file],
});
const collections = await client.listBucketCollections("company-docs");
// Legacy collection aliases remain available for older integrations
const col = await client.getCollection("bucket-id");
await client.deleteCollection("bucket-id");Bucket search is permission-scoped: the server searches only the child collections the caller can access and merges the ranked results.
Document Metadata And Connectors
Uploading a document or syncing it in through a connector (Notion, Gmail, Obsidian, …) attaches metadata you can filter on at search time.
Reserved keys — populated structurally (connector API field mapping or Markdown frontmatter parsing, never LLM-inferred). Availability depends on what the source provides.
| Key | Meaning | Always set? |
|---|---|---|
| source_kind | Kind of source (notion_page, email, markdown_note, upload, …) | yes |
| source_created_at | Original creation time (ISO 8601) | if source provides it |
| source_modified_at | Original last-modified time (ISO 8601) | if source provides it |
| source_author | Author | if source provides it |
| source_uri | Deep link back to the original source | if source provides it |
| source_filename | File name | uploads always; connectors if provided |
| source_tags | Tags/labels from the source (JSON-encoded array string) | if source provides it |
| ingested_by | Who/what ingested it (upload or connector:<provider>) | yes |
| ingested_at | When Schift received the document | yes |
| cclg_source | Internal lineage identifier | yes |
Avoid naming custom metadata keys the same as these — the system value takes precedence.
For Markdown uploads (.md/.markdown), pass parseFrontmatter: true to
promote YAML frontmatter (created/date/author/tags) into the
reserved keys above:
await client.db.upload("company-docs", {
files: [file],
parseFrontmatter: true,
});Custom metadata is always explicit — Schift does not infer business metadata from document content:
await client.db.upload("company-docs", {
files: [file],
metadata: { department: "finance", priority: "high" },
});Custom metadata is set per document and inherited by every chunk derived from it. Values must be strings, numbers, or booleans (coerced to strings server-side).
Filtering — reserved and custom keys are filtered the same way, with
flat, top-level keys (not a nested { metadata: {...} } object):
const resp = await client.search({
query: "revenue guidance",
bucket: "company-docs",
filters: {
source_kind: "email",
department: "finance",
},
});There is no contains operator today — use like ({ like: "%urgent%" })
for substring matches, including against JSON-encoded fields like
source_tags.
Workflows
Build and run RAG pipelines as composable DAGs.
One-block RAG (recommended)
BlockType.RAG is a single composite block that does retrieve + prompt + LLM
inline. No graph wiring required.
import { WorkspaceClient, BlockType } from "@schift-io/sdk";
const client = new WorkspaceClient({
apiKey: process.env.SCHIFT_API_KEY!,
baseUrl: process.env.SCHIFT_API_URL!,
});
const wf = await client.workflows.create({ name: "fire-code-qa" });
await client.workflows.addBlock(wf.id, {
type: BlockType.RAG,
config: { collection: "fire-code" }, // bucket name is the only required field
});
const run = await client.workflows.run(wf.id, {
query: "소방 설비는 얼마나 자주 점검해야 하나요?",
});
// run.outputs → { answer, sources, text, data, usage, results }Advanced config (all optional, sensible defaults): top_k, mode, filter,
rerank, model, temperature, max_tokens, thinking_budget,
system_prompt, template, response_schema, include_sources.
Runtime overrides (passed via workflows.run() inputs): bucket, filter,
tags, top_k, response_schema.
Direct RAG endpoint (skip workflow entirely)
When you don't need a persisted workflow record, hit /v1/rag/run directly —
shares the same code path, less overhead, structured output supported.
That shared runtime is intentionally modular inside the server while staying
fused on the serving path: retrieval resolves the bucket and fetches hits,
context-build formats the prompt evidence, generation renders/calls the LLM,
and evidence-format returns sources/results to the caller. Keep custom prompt
templates on workflow RAG config or /v1/rag/run; public /v1/chat owns its
server-built RAG prompt and rejects client-supplied system_prompt.
The TypeScript SDK mirrors that boundary: client.chat({ systemPrompt: ... })
and client.chatStream({ systemPrompt: ... }) fail locally before a request is
sent.
const apiBaseUrl = process.env.SCHIFT_API_URL ?? "https://api.example.com";
const resp = await fetch(`${apiBaseUrl}/v1/rag/run`, {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.SCHIFT_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
query: "소방 점검 주기?",
bucket: "fire-code",
response_schema: {
type: "object",
properties: { months: { type: "integer" }, cite: { type: "string" } },
},
}),
});
const { answer, data, sources } = await resp.json();Legacy multi-block pipelines
For custom pipelines (multi-source merge, conditional routing, tool use) you
can still build the graph from primitive blocks (retriever, llm,
prompt_template, etc.).
// Create from template or blank
const wf = await client.workflows.create({ name: "My RAG Pipeline" });
// Run with inputs (multiple values supported)
const result = await client.workflows.run(wf.id, {
query: "maternity leave policy",
language: "ko",
});
console.log(result.outputs);CRUD
const wf = await client.workflows.create({ name: "Pipeline" });
const all = await client.workflows.list();
const one = await client.workflows.get(wf.id);
const updated = await client.workflows.update(wf.id, { name: "Renamed" });
await client.workflows.delete(wf.id);Blocks & Edges
// Add blocks
const retriever = await client.workflows.addBlock(wf.id, {
type: "retriever",
title: "Search Docs",
config: { collection: "my-docs", top_k: 5, rerank: true },
});
const llm = await client.workflows.addBlock(wf.id, {
type: "llm",
config: {
model: "openai/gpt-4o-mini", // or "anthropic/claude-sonnet-4-20250514", "gemini-2.5-flash"
temperature: 0.7,
},
});
// Connect blocks
await client.workflows.addEdge(wf.id, {
source: retriever.id,
target: llm.id,
});
// Remove
await client.workflows.removeBlock(wf.id, retriever.id);
await client.workflows.removeEdge(wf.id, edgeId);WorkflowBuilder (Fluent API)
Build a graph locally, then send to the API in one call:
import { WorkflowBuilder } from "@schift-io/sdk";
const request = new WorkflowBuilder("My RAG Pipeline")
.description("Retrieval-augmented generation")
.addBlock("start", { type: "start" })
.addBlock("retriever", {
type: "retriever",
config: { collection: "my-docs", top_k: 5 },
})
.addBlock("prompt", {
type: "prompt_template",
config: { template: "Context:\n{{results}}\n\nQ: {{query}}" },
})
.addBlock("llm", {
type: "llm",
config: { model: "openai/gpt-4o-mini" },
})
.addBlock("end", { type: "end" })
.connect("start", "retriever")
.connect("retriever", "prompt")
.connect("prompt", "llm")
.connect("llm", "end")
.build();
const wf = await client.workflows.create(request);YAML Import / Export
// Export
const yaml = await client.workflows.exportYaml(wf.id);
// Import from YAML string
const imported = await client.workflows.importYaml(yamlString);Validation & Meta
// Validate graph
const { valid, errors } = await client.workflows.validate(wf.id);
// List available block types
const blockTypes = await client.workflows.getBlockTypes();
// List available templates
const templates = await client.workflows.getTemplates();Block Types
| Category | Types |
|----------|-------|
| Control | start, end, conditional, loop |
| Retrieval | retriever, reranker |
| LLM | llm, prompt_template, answer |
| Data | document_loader, chunker, embedder, text_processor |
| Web | web_search |
| Integration | api_call, webhook, code_executor |
| Storage | vector_store, cache |
Configuration
const client = new WorkspaceClient({
apiKey: "sch_...", // required
baseUrl: process.env.SCHIFT_API_URL, // required unless SCHIFT_API_URL/SCHIFT_BASE_URL is set
timeout: 60_000, // default, in milliseconds
});Error Handling
import {
WorkspaceClient,
AuthError,
QuotaError,
PlatformError,
} from "@schift-io/sdk";
const client = new WorkspaceClient({
apiKey: process.env.SCHIFT_API_KEY!,
baseUrl: process.env.SCHIFT_API_URL!,
});
try {
await client.embed({ text: "test" });
} catch (err) {
if (err instanceof AuthError) {
// 401: Invalid or expired API key
} else if (err instanceof QuotaError) {
// 402: Insufficient credits
} else if (err instanceof PlatformError) {
// Other API errors (403, 422, 429, 500, 502)
console.error(err.message, err.status);
}
}Supported Models
| Model | Provider | Dimensions |
|-------|----------|------------|
| openai/text-embedding-3-small | OpenAI | 1536 |
| openai/text-embedding-3-large | OpenAI | 3072 |
| gemini/text-embedding-004 | Google | 768 |
| voyage/voyage-3-large | Voyage | 1024 |
| schift-embed-1-small | Schift | 1024 |
All models output to a canonical 1024-dimensional space via Schift's projection layer.
Releases
Published from the schift-io/schift monorepo to npm via
.github/workflows/sdk-publish-ts.yml. Cut a release with:
# 1. Bump version
$EDITOR clients/sdk/ts/package.json # version: "0.X.Y"
git add clients/sdk/ts/package.json && git commit -m "chore(sdk-ts): bump 0.X.Y"
git push origin main
# 2. Create the release (tag pattern: npm-v*)
gh release create npm-v0.X.Y \
--title "@schift-io/sdk v0.X.Y" \
--notes "..."The workflow verifies the tag matches package.json, runs tsc, npm test,
npm run build, then npm publish --access public.
The public mirror at
schift-io/schift-tsis sync-only (no workflows there). All publish automation lives in the monorepo.
License
MIT
