Agent Apps Docs

Intents

Enable app-to-app communication with ctx.intent() and manifest requires.

Intents

Intents let your app call tools on other installed apps without directly knowing which installation to target. Your app declares what it needs (a messaging tool, an email sender), and the Station resolves the binding at runtime — connecting your call to the right installed instance of the target app.

This is the foundation of composable agent apps: a CRM app can send emails through Gmail, a monitoring app can post alerts to Slack, and neither needs to hardcode the other's configuration.

How intents work

Your app  ──ctx.intent('messenger', 'send_message', {…})──►  Station
                                                                 │
                                                                 │ resolves binding
                                                                 ▼
                                                            Target app (e.g. app.slack)
                                                                 │
                                                                 │ calls tool
                                                                 ▼
                                                            Returns result ◄── back to your app
  1. Your app declares a dependency in its manifest: requires: [{ name: 'messenger', tools: ['send_message'] }].
  2. At install time, the user binds the dependency name (messenger) to a specific installed app (e.g. app.slack).
  3. At runtime, your app calls ctx.intent('messenger', 'send_message', { ... }).
  4. The Station resolves the binding, opens a session to the target app, invokes the tool, and returns the result.
  5. If the binding is missing or the tool call fails, an error is thrown.

Your app never references the target app's ID directly — it only knows the local dependency name and the tool name. This decoupling is what makes apps composable: the same CRM app can send email through Gmail, Outlook, or any future email app, as long as the user binds the dependency and the target app exposes the required tools.


Declare dependencies in the manifest

Add a requires array to your agentapp.json (or createApp() config). Each entry describes one app-to-app dependency:

FieldTypeRequiredDescription
namestringYesLocal dependency name. This is what you pass to ctx.intent(name, ...) in your code.
toolsstring[]YesTool names the target app must expose. At least one is required. This is a structural contract — the Station verifies the bound app provides these tools.
suggeststringNoCanonical app ID hint for the binding UI (e.g. 'app.slack'). Helps the user pick the right app when binding.
optionalbooleanNoIf true, the app degrades gracefully when the dependency is unbound. Default: false.
{
  "id": "app.example.crm",
  "version": "1.0.0",
  "name": "CRM",
  "requires": [
    {
      "name": "email",
      "tools": ["send_email"],
      "suggest": "app.google.gmail",
      "optional": true
    },
    {
      "name": "messenger",
      "tools": ["send_message"],
      "suggest": "app.slack"
    }
  ]
}

In this example:

  • The CRM app requires an email dependency that must expose a send_email tool. It's optional, so the app works without it.
  • The CRM app requires a messenger dependency that must expose a send_message tool. It's required — the app cannot function without it.

Using createApp()

When using the SDK's createApp() helper, pass requires in the config object:

import { createApp } from '@agentapps/sdk';

const app = createApp({
  id: 'app.example.crm',
  version: '1.0.0',
  name: 'CRM',
  description: 'A CRM that sends emails and Slack messages via other apps',
  author: { name: 'Example', email: 'dev@example.com' },
  category: 'productivity',
  runtime: { transport: 'stdio', command: 'node', args: ['./dist/index.js'] },
  requires: [
    {
      name: 'email',
      tools: ['send_email'],
      suggest: 'app.google.gmail',
      optional: true,
    },
    {
      name: 'messenger',
      tools: ['send_message'],
      suggest: 'app.slack',
    },
  ],
});

Call intents from your tools

Use ctx.intent(binding, tool, args) inside any tool handler to invoke a tool on a bound app:

app.tool('notify_deal_closed', {
  description: 'Send a notification when a deal is closed',
  parameters: {
    type: 'object',
    properties: {
      dealName: { type: 'string' },
      amount: { type: 'number' },
      channel: { type: 'string', description: 'Slack channel to notify' },
    },
    required: ['dealName', 'amount', 'channel'],
  },
}, async (ctx, args) => {
  // Call the bound messenger app's send_message tool
  const result = await ctx.intent('messenger', 'send_message', {
    channel: args.channel,
    text: `🎉 Deal closed: ${args.dealName} ($${args.amount.toLocaleString()})`,
  });

  return {
    text: `Notification sent to ${args.channel}`,
    data: result,
  };
});

Parameters

ParameterTypeDescription
bindingstringThe local dependency name from your requires manifest (e.g. 'messenger', 'email').
toolstringThe tool name within the bound target app (e.g. 'send_message', 'send_email').
argsRecord<string, unknown>Optional. Arguments to pass to the target tool.

Return value

ctx.intent() returns Promise<unknown> — the result from the target tool. The shape depends on the target app's tool definition.


Error handling

ctx.intent() throws if the binding is missing, the target app is not installed, or the tool call fails. Always wrap intent calls in try/catch:

app.tool('send_followup_email', {
  description: 'Send a follow-up email to a lead',
  parameters: {
    type: 'object',
    properties: {
      to: { type: 'string' },
      subject: { type: 'string' },
      body: { type: 'string' },
    },
    required: ['to', 'subject', 'body'],
  },
}, async (ctx, args) => {
  try {
    const result = await ctx.intent('email', 'send_email', {
      to: args.to,
      subject: args.subject,
      body: args.body,
    });
    return { text: 'Email sent successfully', data: result };
  } catch (err) {
    // The error may include a `code` and `details` from the target app
    const code = (err as Error & { code?: string }).code;
    const details = (err as Error & { details?: unknown }).details;

    if (code === 'BINDING_NOT_FOUND') {
      return { text: 'No email app is bound. Please bind an email app in settings.' };
    }

    return {
      text: `Failed to send email: ${(err as Error).message}`,
      data: { code, details },
    };
  }
});

Common error codes

CodeMeaning
BINDING_NOT_FOUNDNo app is bound to the dependency name. The user needs to bind an app in the Station's binding UI.
TOOL_NOT_FOUNDThe bound app does not expose the requested tool. Check that the tools array in your requires manifest matches the target app's tool names.
TOOL_EXECUTION_FAILEDThe target app's tool threw an error. The details field may contain more context from the target app.

Optional dependencies

When a dependency is marked optional: true in the manifest, your app should handle the case where no app is bound. Use try/catch and check for BINDING_NOT_FOUND:

app.tool('create_contact', {
  description: 'Create a new contact and optionally send a welcome email',
  parameters: {
    type: 'object',
    properties: {
      name: { type: 'string' },
      email: { type: 'string' },
    },
    required: ['name', 'email'],
  },
}, async (ctx, args) => {
  // Save the contact (core functionality — always works)
  const contact = await ctx.storage.set(`contact:${args.email}`, {
    name: args.name,
    email: args.email,
    createdAt: new Date().toISOString(),
  });

  // Try to send a welcome email (optional — degrades gracefully)
  try {
    await ctx.intent('email', 'send_email', {
      to: args.email,
      subject: `Welcome, ${args.name}!`,
      body: `Hi ${args.name}, thanks for joining!`,
    });
    return { text: `Contact created and welcome email sent to ${args.email}` };
  } catch (err) {
    // No email app bound — that's fine, the contact is still saved
    return { text: `Contact created. (Welcome email skipped — no email app bound.)` };
  }
});

Bindings: how the Station resolves dependencies

Bindings are the user's responsibility. When a user installs an app that declares requires, the Station presents a binding UI where the user selects which installed app satisfies each dependency.

Installing app.example.crm v1.0.0
├── Requires: email (send_email)     →  Bind to: app.google.gmail  ✓
├── Requires: messenger (send_message) →  Bind to: app.slack        ✓
└── Optional: (none)

Ready to install

The Station validates that the selected app exposes all the tools listed in the tools array. If the bound app doesn't expose a required tool, the binding is rejected.

Multiple installations

If a user has multiple installations of the same app (e.g. two Gmail accounts), the binding lets them choose which installation to use. Your app doesn't need to know — it just calls ctx.intent('email', 'send_email', ...) and the Station routes it to the bound installation.

Rebinding

Bindings can be changed after installation. If a user uninstalls the bound app, the Station marks the binding as broken and the user must rebind. Your app will receive a BINDING_NOT_FOUND error until a new binding is set.


Use cases

CRM → Email

A CRM app tracks deals and contacts. When a deal closes, it sends a follow-up email through the user's installed email app:

app.tool('close_deal', {
  description: 'Mark a deal as closed and send a notification email',
  parameters: {
    type: 'object',
    properties: {
      dealId: { type: 'string' },
      customerEmail: { type: 'string' },
    },
    required: ['dealId', 'customerEmail'],
  },
}, async (ctx, args) => {
  // Update deal status in storage
  await ctx.storage.set(`deal:${args.dealId}`, {
    status: 'closed',
    closedAt: new Date().toISOString(),
  });

  // Send email via bound email app
  await ctx.intent('email', 'send_email', {
    to: args.customerEmail,
    subject: 'Deal closed — thank you!',
    body: 'Your deal has been successfully processed.',
  });

  return { text: `Deal ${args.dealId} closed and email sent.` };
});

Monitoring → Slack

A monitoring app detects issues and posts alerts to Slack via a bound messenger app:

app.tool('check_service_health', {
  description: 'Check service health and alert on failures',
  parameters: {
    type: 'object',
    properties: {
      serviceUrl: { type: 'string' },
      alertChannel: { type: 'string', description: 'Slack channel for alerts' },
    },
    required: ['serviceUrl', 'alertChannel'],
  },
}, async (ctx, args) => {
  const response = await fetch(args.serviceUrl);
  const healthy = response.ok;

  if (!healthy) {
    // Alert the team via bound messenger app
    await ctx.intent('messenger', 'send_message', {
      channel: args.alertChannel,
      text: `🚨 Service down: ${args.serviceUrl} returned ${response.status}`,
    });
  }

  return {
    text: healthy ? 'Service is healthy' : `Service is down — alert sent to ${args.alertChannel}`,
    data: { status: response.status, healthy },
  };
});

Project Manager → Calendar + Email

A project management app uses two dependencies — a calendar app for scheduling and an email app for notifications:

{
  "requires": [
    {
      "name": "calendar",
      "tools": ["create_event", "list_events"],
      "suggest": "app.google.calendar"
    },
    {
      "name": "email",
      "tools": ["send_email"],
      "suggest": "app.google.gmail",
      "optional": true
    }
  ]
}
app.tool('schedule_meeting', {
  description: 'Schedule a meeting and email attendees',
  parameters: {
    type: 'object',
    properties: {
      title: { type: 'string' },
      date: { type: 'string' },
      attendees: { type: 'array', items: { type: 'string' } },
    },
    required: ['title', 'date', 'attendees'],
  },
}, async (ctx, args) => {
  // Create calendar event via bound calendar app
  const event = await ctx.intent('calendar', 'create_event', {
    title: args.title,
    date: args.date,
    attendees: args.attendees,
  });

  // Optionally email attendees
  try {
    await ctx.intent('email', 'send_email', {
      to: args.attendees.join(', '),
      subject: `Meeting scheduled: ${args.title}`,
      body: `You've been invited to a meeting on ${args.date}.`,
    });
  } catch {
    // Email is optional — calendar event was still created
  }

  return { text: `Meeting "${args.title}" scheduled for ${args.date}.`, data: event };
});

Best practices

  • Use descriptive dependency names. messenger, email, calendar are clear. Avoid generic names like app1 or service.
  • Declare only the tools you use. The tools array is a contract — listing tools you don't call adds unnecessary constraints on the bound app.
  • Mark truly optional dependencies as optional: true. This tells the Station and the user that your app works without them.
  • Always handle BINDING_NOT_FOUND for optional dependencies. For required dependencies, a clear error message is fine — the user must bind before the app can work.
  • Use suggest to guide binding. If your app is designed to work with a specific app (e.g. Slack for messaging), set suggest to that app's ID. The user can still bind a different app, but the hint improves the UX.