AI Connect/How to create/Department copilots (logistics)

Department copilots (logistics)

Ask AI about Vinkius

Model every department of a logistics company as its own AI Connect SDK user, Finance, Dispatch, Warehouse and Fleet Ops, each with its own connectors, credentials and capabilities, on one application key.

Here is what an internal AI platform was never allowed to look like until now: a logistics company where Finance reconciles invoices, Dispatch answers "where is load 4412?", Warehouse counts the dock, Fleet Ops tracks maintenance, and every team gets its own copilot with its own connectors and credentials, on one Application key, backed by thousands of AI connections from day one. NetSuite, the ERP, the WMS, a telematics API, Slack channels, Google Sheets: no integration built by you, and each team's credentials belong to the team, not to whichever user is currently logged in.

Every integration platform you have ever evaluated stops exactly there. They connect an application to a service. None of them has ever offered a team, a department, a shared role as a first-class user with its own isolated credentials, because none of them has a user model that can name anything but a human login. The AI Connect SDK can, and that reframe is this build: the department is the user. One external_id per team. The humans are just who operates the copilot; the isolation boundary is the department. The same architecture that serves one person with one Gmail serves the whole org chart, with no new infrastructure, no new platform, no new vendor conversation. You scale an organization by adding ids, not by buying software.

Agent loop · one turn
One user turn of the quickstart, exactly as the console serves it. Click a step or press Run.
vinkius.user('alice_123').capabilities({ include: ['github'] })
HTTPGET /apps/vk_app_xxx/users/alice_123/tools?connector=github
const capabilities = await vinkius
  .user('alice_123')
  .capabilities({ include: ['github'] });
CapabilitySet (6)
  github__list_issues        read-only
  github__create_issue       POST /repos/{owner}/{repo}/issues
  github__list_pull_requests read-only
  github__search_code        read-only
  ...
Step 1 of 6
The agent loop, step by step. Press Run and follow one user turn: capabilities load, convert to tools, the model calls github__create_issue, the SDK executes on that user connection and the result feeds back. Every step shows the real SDK call and its HTTP request.

The same loop you saw for a human now runs against dept-dispatch instead of alice_123. The copilot acts on Dispatch's own TMS and Slack, never on Finance's NetSuite, because a connection belongs to whoever owns the external_id.

One Application keyvk_app_*dept-financeown credentialsdept-dispatchown credentialsdept-warehouseown credentialsdept-fleetown credentialsisolatedper external_id
The org chart as user table: each department owns its connections, credentials and spend, isolated by external_id under one Application key.

Why a department is a perfect "user"

  • Shared, durable state. Nobody "owns" the connection; dept-warehouse owns it. Staff turnover never breaks the integration.
  • Blast-radius isolation. Each connection carries its own data-plane token, so Dispatch's spend and instant revocation are independent from Warehouse's.
  • Least privilege by construction. A copilot literally cannot see a connector another department connected, the capability query is scoped to one external_id.
  • One application key. All departments live under a single Vinkius Application. You add a team by defining a new id, not by provisioning infrastructure.

1. Name the departments

Use a stable, readable, URL-safe id. Prefix them so they never collide with a human id from another part of your system.

typescript
type Department = 'finance' | 'dispatch' | 'warehouse' | 'fleet';

const departmentUserId = (dept: Department) => `dept-${dept}`;
// "dept-finance", "dept-dispatch", "dept-warehouse", "dept-fleet"

2. Declare each department's connectors

Different teams need different tools. Keep that as configuration, the rest of the code never changes per team.

typescript
// server/departments.ts
interface DeptSpec {
  label: string;
  connectors: string[]; // catalog slugs
}

export const DEPARTMENTS: Record<Department, DeptSpec> = {
  finance:   { label: 'Finance',    connectors: ['netsuite', 'stripe', 'gmail'] },
  dispatch:  { label: 'Dispatch',   connectors: ['sap', 'slack', 'google-sheets'] },
  warehouse: { label: 'Warehouse',  connectors: ['wms', 'google-sheets', 'jira'] },
  fleet:     { label: 'Fleet Ops',  connectors: ['telematics', 'servicemax', 'slack'] },
};

Connector slugs come from the live catalog. Let your admin UI discover them instead of hardcoding: await vinkius.catalog.search('telematics') or for await (const c of vinkius.catalog.iterate()). See Connectors & credentials.

3. Provision a department once (an admin action)

When a team is onboarded, connect its accounts and store the credentials. The department id is the externalId everywhere, there is no human in this path.

typescript
import { vinkius } from './vinkius';
import { DEPARTMENTS, type Department } from './departments';

async function bootstrapDepartment(dept: Department) {
  const user = vinkius.user(`dept-${dept}`);

  // attach non-secret metadata so you can filter/audit later
  await user.ensure({ kind: 'department', label: DEPARTMENTS[dept].label });

  for (const slug of DEPARTMENTS[dept].connectors) {
    const connector = user.connector(slug);
    await connector.connect();
    // api_key connectors: connector.credentials.set({ API_KEY: ... })
    // oauth connectors: connect() returns after provider consent
  }

  // report readiness per connector so admins see what still needs credentials
  return Promise.all(
    DEPARTMENTS[dept].connectors.map(async (slug) => ({
      slug,
      status: await user.connector(slug).status(),
    })),
  );
}

4. Load the department's capabilities at request time

A request arrives with the department the copilot serves. Resolve its capabilities, scoped and ready to pass to the model.

typescript
async function departmentCapabilities(dept: Department) {
  const spec = DEPARTMENTS[dept];
  return vinkius.user(`dept-${dept}`).capabilities({
    include: spec.connectors,
    onConnectorError: (slug, error) => {
      // surface to admins; do not interrupt the response
      console.warn(`${dept}/${slug}`, (error as Error).message);
    },
  });
}

5. Answer as the department

Route the message to the right copilot, give the model that department's tools only, execute on that department's connection.

typescript
// server/copilot.ts
import OpenAI from 'openai';
import { toOpenAITools, runOpenAIToolCall } from '@vinkius/connect/openai';
import { type Department } from './departments';
import { departmentCapabilities } from './capabilities';

const openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY! });

const SYSTEM_PROMPT: Record<Department, string> = {
  finance: 'You are the Finance copilot. Reconcile invoices and answer billing questions using the connected accounting tools.',
  dispatch: 'You are the Dispatch copilot. Answer where loads are and update ETAs using the TMS and Slack.',
  warehouse: 'You are the Warehouse copilot. Report dock counts and open tasks using the WMS and Sheets.',
  fleet: 'You are the Fleet Ops copilot. Report vehicle health and maintenance using telematics and ServiceMax.',
};

export async function ask(dept: Department, question: string) {
  const capabilities = await departmentCapabilities(dept);

  const completion = await openai.chat.completions.create({
    model: '[MODEL_ID]',
    messages: [
      { role: 'system', content: SYSTEM_PROMPT[dept] },
      { role: 'user', content: question },
    ],
    tools: toOpenAITools(capabilities),
  });

  const call = completion.choices[0]?.message.tool_calls?.[0];
  if (!call) return { answer: completion.choices[0].message.content };

  const result = await runOpenAIToolCall(capabilities, call); // runs as this dept
  return { answer: result.content, tool: call.function.name };
}
typescript
// Dispatch asks about its own TMS; it cannot reach Finance's NetSuite.
await ask('dispatch', 'What is the ETA for load 4412 and which driver is on it?');
await ask('finance', 'Which customer invoices over 30 days are still open this month?');

6. Let humans act as a department, with an audit trail

The people operating a copilot are not the "user" in the SDK sense, but you should still know who asked. Log the operator alongside the department id, the capability boundary stays with the department.

typescript
async function askAs(dept: Department, operator: string, question: string) {
  const answer = await ask(dept, question);
  // your own audit store — the SDK never sees the operator
  await audit.record({
    actor: operator,
    on_behalf_of: `dept-${dept}`,
    question,
  });
  return answer;
}

Keep the operator identity entirely in your system. To Vinkius, the actor is always dept-finance. That is what gives you department-level isolation and lets the same copilot behave identically no matter which employee is typing.

7. Grant and revoke a team in a single operation

Because a department is one external_id, decommissioning is trivial. Disconnecting a connector removes only that team's access.

typescript
async function retireConnector(dept: Department, slug: string) {
  await vinkius.user(`dept-${dept}`).connector(slug).disconnect();
}

Power features worth knowing here

One department, one connector, without re-listing everything

A focused copilot that only ever accesses one system should not incur the cost of a fan-out over the whole team's connections. forConnector slices an already-loaded set; and to skip fetching the others entirely, list from a single connector handle:

typescript
// from an aggregated set:
const sheetsOnly = capabilities.forConnector('google-sheets');

// or avoid listing every connection at all:
const tmsOnly = await vinkius.user(`dept-${dept}`).connector('sap').capabilities();

The fan-out is concurrent and self-healing

user.capabilities() lists the connection summaries once, then fans out to the ready connectors with a concurrency cap of 8 and reuses each resolved connection id (no per-connector re-list). A runtime timeout on one connector does not interrupt the response, the rest still resolve, and onConnectorError reports which team tool was unavailable so you can warn that department's administrator.

Read a department's own metadata back

The non-secret kind: 'department' you attached with ensure() is available for you to render. user.get() returns the stored metadata and status, enough to build a "which teams are onboarded?" dashboard without any extra database.

typescript
const profile = await vinkius.user('dept-finance').get();
console.log(profile.metadata); // { kind: 'department', label: 'Finance' }
console.log(profile.status);   // active | ...

Production checklist

  • [ ] Prefix department ids so they can never collide with human ids (dept-…).
  • [ ] Store the per-department connector list as configuration, not code branches.
  • [ ] Onboard with user.ensure({ kind: 'department' }) so metadata-driven dashboards work.
  • [ ] Render connector.status() in the admin console so gaps (needs_credentials) are visible.
  • [ ] Route human operators to their department copilot; keep who-asked logging on your side.
  • [ ] Give every mutating copilot action a stable idempotencyKey (e.g. ticket + dept).

You now run an internal AI platform where Finance, Dispatch, Warehouse and Fleet each have their own connected tools and their own copilot, all from one application key and one external_id per team. You did not buy a platform for departments; the platform simply has no ceiling on what a user can be. Adding a fifth team tomorrow is one line of configuration. That is what it means to own the connectivity layer: the org chart becomes your user table.

What you just got

Not a pitch: the properties this build inherits automatically.

Isolation by construction

Connections and capabilities resolve only inside one external_id. No cross-actor leakage is possible, and you wrote none of that enforcement.

Write-only credentials

Your server stores secrets and can read back which fields are configured, never the values. Not your code, the model, or a dashboard can exfiltrate them.

Metered, revocable spend

Every connection owns a vk_live_* token, so cost and revocation are per connection. One call to disconnect() is a complete, auditable stop.

Any model runtime

One CapabilitySet converts to OpenAI, Anthropic, Gemini, Vercel AI SDK, LangChain, LlamaIndex, Workers AI or neutral JSON Schema. Only the last line changes.

Production safety built in

idempotencyKey, timeoutMs and AbortSignal per call; automatic full-jitter retries on transient failures; typed VinkiusError branches. No bespoke harness.

Least privilege per team

A copilot literally cannot see another department’s connectors. Onboarding a team is a new external_id, not new infrastructure.

Give it to your AI agent

An Agent Skill (SKILL.md) for this build. Preview the first lines below, then copy or download it into your repo under .claude/skills/: Claude Code, Cursor or any Agent-Skills-compatible agent follows it to implement this pattern correctly.

Download SKILL.md6 · Available in your language
What department copilots inherit, plus the SKILL.md, in your language, for your coding agent.

Next steps