Skip to content

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')
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.
  • scopesRequired must be a subset of manifest.required_scopes.
  • app.onX requires manifest.lifecycle.X to be true.
  • Schedule names must match manifest.lifecycle.schedules[].name.
  • Double-registration throws.
await app.run() // production
await app.run({ local: true }) // local (MockTools + MockSecrets)
interface Context {
readonly tenantId: string
readonly appId: string
readonly appVersion: string
readonly tools: ToolsApi
readonly secrets: SecretsApi
}
const result = await ctx.tools.call('email_send', {
subject: 'Hello',
body: '',
})

Throws ToolCallError on platform-side denial / failure (same decision codes as Python).

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).

class ToolCallError extends Error {
readonly toolName: string
readonly decision: string // 'scope_denied' | 'network_error' | …
readonly neededScopes: string[]
}
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:

Terminal window
POST /__mock/tool { "name": "email_send", "result": { "sent": true } }
POST /__mock/secret { "key": "OPENAI_KEY", "value": "sk-test" }
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>
}
loadManifest('linkworld.app.yaml') // string → Manifest, or ManifestError
loadManifestFromString('apiVersion: …') // string → Manifest, or ManifestError

Both throw ManifestError (wraps the underlying Zod issue list) on parse or schema failure.

PythonTypeScript
@app.tool('name', ...) decoratorapp.tool('name', ...) builder
@app.on_inboundapp.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 InboundEnvelopefrom (no Python keyword conflict)