TypeScript SDK reference
import { App, Context, InboundEnvelope, MockSecrets, MockTools, ToolCallError, loadManifest, loadManifestFromString,} from '@linkworld/sdk'Requires Node >= 20 (uses native fetch, node:http, node:test).
const app = App.fromManifest('linkworld.app.yaml')Builder methods
Section titled “Builder methods”app.tool('classify', { description: '...', scopesRequired: ['mail.send'] }, async (ctx, args) => { return { category: 'tax' } },)
// Two-arg form when no options are needed:app.tool('quickstub', async (_ctx, args) => ({ ok: 1 }))
app.onInstall(async (ctx) => { /* ... */ })app.onUninstall(async (ctx) => { /* ... */ })app.onInbound(async (ctx, env) => { /* env is InboundEnvelope */ })app.onUserAdded(async (ctx, user) => { /* ... */ })app.onSchedule('daily-summary', async (ctx) => { /* ... */ })Same registration rules as the Python SDK:
- Handler must return a Promise.
scopesRequiredmust be a subset ofmanifest.required_scopes.app.onXrequiresmanifest.lifecycle.Xto be true.- Schedule names must match
manifest.lifecycle.schedules[].name. - Double-registration throws.
app.run({ local?, host?, port? })
Section titled “app.run({ local?, host?, port? })”await app.run() // productionawait app.run({ local: true }) // local (MockTools + MockSecrets)Context
Section titled “Context”interface Context { readonly tenantId: string readonly appId: string readonly appVersion: string readonly tools: ToolsApi readonly secrets: SecretsApi}ctx.tools.call(name, args)
Section titled “ctx.tools.call(name, args)”const result = await ctx.tools.call('email_send', { subject: 'Hello', body: '…',})Throws ToolCallError on platform-side denial / failure (same
decision codes as Python).
ctx.secrets.get(key)
Section titled “ctx.secrets.get(key)”const apiKey = await ctx.secrets.get('OPENAI_KEY')if (apiKey === null) { throw new Error('Tenant did not configure OPENAI_KEY')}Returns string | null. Never throws — fail-closed on network error.
Key must match ^[A-Z][A-Z0-9_]{0,63}$ (validated SDK-side).
ToolCallError
Section titled “ToolCallError”class ToolCallError extends Error { readonly toolName: string readonly decision: string // 'scope_denied' | 'network_error' | … readonly neededScopes: string[]}MockTools / MockSecrets
Section titled “MockTools / MockSecrets”const tools = new MockTools()tools.register('email_send', async () => ({ sent: true }))
const secrets = new MockSecrets({ OPENAI_KEY: 'sk-test' })secrets.set('STRIPE_KEY', 'sk_test_…')In local mode, the runtime attaches these automatically and exposes:
POST /__mock/tool { "name": "email_send", "result": { "sent": true } }POST /__mock/secret { "key": "OPENAI_KEY", "value": "sk-test" }InboundEnvelope
Section titled “InboundEnvelope”interface InboundEnvelope { message_id: string channel: string // 'email' | 'whatsapp' | 'webhook' | … from: string body: string subject: string | null received_at: string // ISO-8601 UTC attachment_ids: string[] metadata: Record<string, unknown>}Manifest helpers
Section titled “Manifest helpers”loadManifest('linkworld.app.yaml') // string → Manifest, or ManifestErrorloadManifestFromString('apiVersion: …') // string → Manifest, or ManifestErrorBoth throw ManifestError (wraps the underlying Zod issue list) on
parse or schema failure.
Differences from Python
Section titled “Differences from Python”| Python | TypeScript |
|---|---|
@app.tool('name', ...) decorator | app.tool('name', ...) builder |
@app.on_inbound | app.onInbound(fn) |
@app.on_schedule('daily') | app.onSchedule('daily', fn) |
await ctx.tools.call('email_send', to=...) | await ctx.tools.call('email_send', { to }) |
from_ field on InboundEnvelope | from (no Python keyword conflict) |