Stacklink
Reference

TypeScript SDK

Certified Vercel AI SDK, OpenAI Agents SDK, and Claude Agent SDK recipes.

Stacklink supplies executable, governed tools. Your selected agent harness owns the model call, tool loop, and final answer.

npm install @stacklink/sdk

Create a project API key in Stacklink Overview, keep it in STACKLINK_API_KEY, and use the complete recipe for your harness below. The API key is for code using the SDK; MCP app connections use OAuth instead.

Vercel AI SDK

Install the Stacklink SDK with the selected harness:

npm install @stacklink/sdk ai@^7.0.0 @ai-sdk/openai@^4.0.0

Required environment: STACKLINK_API_KEY, OPENAI_API_KEY, OPENAI_MODEL.

STACKLINK_BASE_URL is optional and defaults to https://api.stacklink.in.

agent.ts
import { openai } from '@ai-sdk/openai';
import { ToolLoopAgent } from 'ai';
import { Stacklink } from '@stacklink/sdk';
import { createStacklinkVercelTools, withStacklinkMemory } from '@stacklink/sdk/vercel-ai';

async function main(): Promise<void> {
  const stacklink = new Stacklink({
    apiKey: requireEnvironment('STACKLINK_API_KEY'),
    baseUrl: process.env.STACKLINK_BASE_URL ?? 'https://api.stacklink.in',
  });

  const session = await stacklink.create({
    tenantId: 'acme',
    userId: 'user_123',
    modelSurface: { mode: 'router' },
  });

  const model = withStacklinkMemory(openai(requireEnvironment('OPENAI_MODEL')), { session, availabilityMode: 'throw' });
  try {
    const agent = new ToolLoopAgent({
      model,
      instructions: 'Use Stacklink search, exact contracts, connection management, and governed execution to complete the request. Keep the same retry key for the same action. If an action is pending, inspect that operation; never submit it again with a new key.',
      tools: createStacklinkVercelTools(session),
    });
    const result = await agent.generate({
      prompt: 'Find the best Gmail tool for reading unread messages and explain the next required action.',
    });
    console.log(result.text);
  } finally {
    try {
      await model.flushCaptures();
    } finally {
      // A finished model turn can leave an action waiting for approval or execution.
      let cursor: string | undefined;
      let pending = false;
      do {
        const page = await session.listExecutions({ cursor, limit: 100 });
        pending = page.liveCount > 0;
        cursor = page.nextCursor ?? undefined;
      } while (!pending && cursor);
      if (pending) console.log('Keep this session for the pending operation:', session.id);
      else await session.close();
    }
  }
}

void main().catch((error: unknown) => {
  console.error(error);
  process.exitCode = 1;
});

function requireEnvironment(name: string): string {
  const value = process.env[name];
  if (!value) throw new Error(`${name} is required.`);
  return value;
}

OpenAI Agents SDK

Install the Stacklink SDK with the selected harness:

npm install @stacklink/sdk @openai/agents@^0.13.3

Required environment: STACKLINK_API_KEY, OPENAI_API_KEY, OPENAI_MODEL.

STACKLINK_BASE_URL is optional and defaults to https://api.stacklink.in.

agent.ts
import { Agent, run } from '@openai/agents';
import { Stacklink } from '@stacklink/sdk';
import { createStacklinkOpenAIAgentsTools, withStacklinkMemory } from '@stacklink/sdk/openai-agents';

async function main(): Promise<void> {
  const stacklink = new Stacklink({
    apiKey: requireEnvironment('STACKLINK_API_KEY'),
    baseUrl: process.env.STACKLINK_BASE_URL ?? 'https://api.stacklink.in',
  });

  const session = await stacklink.create({
    tenantId: 'acme',
    userId: 'user_123',
    modelSurface: { mode: 'router' },
  });

  const runner = withStacklinkMemory({ run }, { session, availabilityMode: 'throw' });
  try {
    const agent = new Agent({
      name: 'Stacklink assistant',
      model: requireEnvironment('OPENAI_MODEL'),
      instructions: 'Use Stacklink search, exact contracts, connection management, and governed execution to complete the request. Keep the same retry key for the same action. If an action is pending, inspect that operation; never submit it again with a new key.',
      tools: createStacklinkOpenAIAgentsTools(session),
    });
    const result = await runner.run(
      agent,
      'Find the best Gmail tool for reading unread messages and explain the next required action.',
    );
    console.log(result.finalOutput);
  } finally {
    try {
      await runner.flushCaptures();
    } finally {
      // A finished model turn can leave an action waiting for approval or execution.
      let cursor: string | undefined;
      let pending = false;
      do {
        const page = await session.listExecutions({ cursor, limit: 100 });
        pending = page.liveCount > 0;
        cursor = page.nextCursor ?? undefined;
      } while (!pending && cursor);
      if (pending) console.log('Keep this session for the pending operation:', session.id);
      else await session.close();
    }
  }
}

void main().catch((error: unknown) => {
  console.error(error);
  process.exitCode = 1;
});

function requireEnvironment(name: string): string {
  const value = process.env[name];
  if (!value) throw new Error(`${name} is required.`);
  return value;
}

Claude Agent SDK

Install the Stacklink SDK with the selected harness:

npm install @stacklink/sdk @anthropic-ai/claude-agent-sdk@^0.3.214

Required environment: STACKLINK_API_KEY, ANTHROPIC_API_KEY, CLAUDE_MODEL.

STACKLINK_BASE_URL is optional and defaults to https://api.stacklink.in.

agent.ts
import { query } from '@anthropic-ai/claude-agent-sdk';
import { Stacklink, createStacklinkMemoryLifecycle } from '@stacklink/sdk';
import { createStacklinkClaudeAgentToolConfig } from '@stacklink/sdk/claude-agent';

async function main(): Promise<void> {
  const stacklink = new Stacklink({
    apiKey: requireEnvironment('STACKLINK_API_KEY'),
    baseUrl: process.env.STACKLINK_BASE_URL ?? 'https://api.stacklink.in',
  });

  const session = await stacklink.create({
    tenantId: 'acme',
    userId: 'user_123',
    modelSurface: { mode: 'router' },
  });

  const memory = createStacklinkMemoryLifecycle({ session, availabilityMode: 'throw', awaitCapture: true });
  try {
    const prompt = 'Find the best Gmail tool for reading unread messages and explain the next required action.';
    const recalled = await memory.recall(prompt);
    const stacklinkTools = createStacklinkClaudeAgentToolConfig(session);
    let finalOutput = '';
    for await (const message of query({
      prompt,
      options: {
        model: requireEnvironment('CLAUDE_MODEL'),
        systemPrompt: ['Use Stacklink search, exact contracts, connection management, and governed execution to complete the request. Keep the same retry key for the same action. If an action is pending, inspect that operation; never submit it again with a new key.', recalled?.promptText ?? ''].filter(Boolean).join('\n\n'),
        tools: [],
        maxTurns: 6,
        ...stacklinkTools,
      },
    })) {
      if (message.type === 'result' && message.subtype === 'success') {
        finalOutput = message.result;
        await memory.captureCompletedTurn({ userText: prompt, assistantText: finalOutput, turn: {}, framework: 'claude-agent-sdk' });
      }
    }
    console.log(finalOutput);
  } finally {
    // A finished model turn can leave an action waiting for approval or execution.
    let cursor: string | undefined;
    let pending = false;
    do {
      const page = await session.listExecutions({ cursor, limit: 100 });
      pending = page.liveCount > 0;
      cursor = page.nextCursor ?? undefined;
    } while (!pending && cursor);
    if (pending) console.log('Keep this session for the pending operation:', session.id);
    else await session.close();
  }
}

void main().catch((error: unknown) => {
  console.error(error);
  process.exitCode = 1;
});

function requireEnvironment(name: string): string {
  const value = process.env[name];
  if (!value) throw new Error(`${name} is required.`);
  return value;
}

Shared approvals

Available from SDK 0.3.6. Bots, Studio, Workbench and direct tool calls use the same stored approval. An approval decision does not submit the action again.

Keep the project key on your server. Listing and observation require platform_approvals:read; decisions and customer-grant management require platform_approvals:manage. A key represents your application, not a company approver. Customer decisions additionally require a verified customer session and an active grant covering the exact action, account and resources.

  1. Retrieve the request and show its exact action and values. Use the returned permissions to enable decisions.
  2. After the authorized customer explicitly chooses, submit that choice with the revision they saw. Do not approve automatically when an agent encounters a wait.
  3. Continue observing the original execution. A customer confirmation alone does not satisfy a separate company approval.
import { Stacklink } from "@stacklink/sdk";

const stacklink = new Stacklink({ apiKey: process.env.STACKLINK_API_KEY! });

// Call from your authenticated server handler after the customer chooses.
// Resolve sessionId from your signed-in customer, never from an arbitrary ID.
async function submitCustomerDecision(
  sessionId: string, approvalId: string, displayedRevision: number,
  decision: "approve" | "decline",
) {
  const access = { sessionId, asCustomer: true };
  const current = await stacklink.approvals.get(approvalId, access);
  if (!current.permissions.canDecide || current.permissions.requirement !== "customer") {
    throw new Error("This customer cannot decide this request.");
  }
  return stacklink.approvals.decide(approvalId, access, {
    requirement: "customer", decision, expectedRevision: displayedRevision,
  });
}

Use approvals.list({ sessionId, asCustomer: true }) for the current customer. approvals.observe(access, { after }) yields ordered updates; save each event.sequence and pass it as after after reconnecting. Use getCustomerGrant, saveCustomerGrant and removeCustomerGrant only from your authorized server.

A revision conflict means the displayed proposal is stale: retrieve and display the current request before asking for another decision. A failed notification is separate from a failed action. A final approval decision may still be followed by execution or an execution failure.

Source of truth

These examples are generated from the same executable files used by Stacklink Overview and release certification. npm and PyPI deliver the packages; this documentation remains the onboarding source of truth.

On this page