Agent Apps Docs

Health Checks

Report your app's health status to the Station so it can monitor, restart, and apply backpressure intelligently.

Health Checks

Health checks let your app report its operational status to the Station — the host platform that manages agent app instances. The Station uses this information to decide whether your app is ready to receive tool calls, whether it needs a restart, and whether to apply backpressure under load.

When you register a health check handler, the Station periodically sends a health_check request to your app instance. Your handler probes external dependencies (API keys, OAuth tokens, database connections) and returns a structured result. The Station then acts on it — surfacing issues in the UI, throttling traffic, or restarting the instance.

Health checks are enabled by default. The capabilities.health field in your app config defaults to true, so you only need to register a handler to start reporting.


Registering a health check

Use app.health() to register a handler. The handler receives a HealthContext and returns a HealthResult:

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

const app = createApp({
  id: 'app.weather',
  version: '1.0.0',
  name: 'Weather',
  description: 'Fetch weather data from an external API',
  author: { name: 'Jane Doe', email: 'jane@example.com' },
  category: 'tools',
  runtime: { type: 'docker', image: 'node:22-slim' },
});

app.health(async (ctx) => {
  // Verify the API key is configured
  const secrets = await ctx.secrets.getSystem();
  const apiKey = secrets?.['WEATHER_API_KEY'];

  if (!apiKey) {
    return {
      status: 'not_ready',
      message: 'Weather API key is not configured.',
      issues: [
        {
          code: 'missing_api_key',
          message: 'System secret "WEATHER_API_KEY" is not set.',
          severity: 'error',
          action: {
            type: 'link',
            url: '/settings/apps',
            label: 'Open app settings',
          },
        },
      ],
    };
  }

  // Probe the external API
  try {
    const res = await fetch('https://api.weather.example.com/health', {
      headers: { Authorization: `Bearer ${apiKey}` },
      signal: AbortSignal.timeout(5000),
    });

    if (!res.ok) {
      return {
        status: 'degraded',
        message: 'Weather API returned a non-OK response.',
        issues: [
          {
            code: 'api_unhealthy',
            message: `API responded with ${res.status} ${res.statusText}.`,
            severity: 'warning',
          },
        ],
      };
    }

    return { status: 'ready', message: 'Weather API is reachable.' };
  } catch (err) {
    return {
      status: 'not_ready',
      message: 'Weather API is unreachable.',
      issues: [
        {
          code: 'api_unreachable',
          message: `Failed to reach weather API: ${err instanceof Error ? err.message : String(err)}`,
          severity: 'error',
        },
      ],
    };
  }
});

export default app.toFactory();

The handler is called on every health_check request from the Station. Keep it lightweight — avoid expensive operations that could time out.


HealthContext

The context passed to your health handler is a subset of ToolContext. It does not include an execution context or abort signal (health checks are synchronous probes), but it provides secrets and auth access so you can verify credentials:

FieldTypeDescription
ctx.secretsSecretsAccessRead system and user secrets via host callbacks
ctx.authAuthAccessCheck OAuth status, refresh tokens
ctx.log()(level, message, data?) => voidEmit a log event to the host

Use ctx.secrets.getSystem() to retrieve app-wide API keys, or ctx.secrets.getUser() for per-user credentials. Use ctx.auth.getStatus() to check whether the user is authenticated with an OAuth provider.


HealthResult

Your handler must return a HealthResult object:

FieldTypeRequiredDescription
status'ready' | 'not_ready' | 'degraded'YesOverall health status
messagestringNoHuman-readable summary shown in the Station UI
issuesHealthIssue[]NoStructured issues for detailed UI display

Status values

StatusMeaningStation behavior
readyAll dependencies are healthyApp receives traffic normally
degradedMinor issues — app works but with limitationsStation may reduce traffic or show warnings
not_readyCritical failure — app cannot serve requestsStation stops routing tool calls, may restart the instance

HealthIssue

Each issue provides structured detail that the Station can render as actionable UI:

interface HealthIssue {
  /** Machine-readable code, e.g. 'missing_api_key' */
  code: string;
  /** Human-readable explanation */
  message: string;
  /** 'error' makes the overall status 'not_ready'; 'warning' makes it 'degraded' */
  severity: 'error' | 'warning';
  /** Optional action the user can take to resolve the issue */
  action?: {
    type: 'link' | 'oauth';
    url: string;
    label: string;
  };
}

The action field is particularly powerful — it lets the Station render a button or link that the user can click to fix the problem directly (e.g., "Open app settings" to configure a missing secret, or "Connect Google" to start an OAuth flow).


HealthCheckBuilder

While you can construct HealthResult objects manually, the SDK provides a fluent HealthCheckBuilder that simplifies common checks — verifying secrets, auth, and API connectivity — and automatically aggregates severities into the correct status.

Basic usage

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

const app = createApp({ /* ... */ });

app.health(async (ctx) => {
  const builder = new HealthCheckBuilder(ctx.secrets, ctx.auth);

  // Verify the system secret is configured
  await builder.requireSystemSecret('WEATHER_API_KEY');

  // Verify the user is authenticated with Google
  await builder.requireAuth('google');

  // Probe the external API
  await builder.requireApiAccess(
    'https://api.weather.example.com/health',
    async (url) => {
      const secrets = await ctx.secrets.getSystem();
      const res = await fetch(url, {
        headers: { Authorization: `Bearer ${secrets?.['WEATHER_API_KEY']}` },
      });
      return res.ok;
    },
    {
      label: 'Weather API',
      timeoutMs: 5000,
      helpUrl: 'https://status.weather.example.com',
    },
  );

  return builder.build('Weather service health check completed');
});

Builder methods

MethodDescription
addIssue(issue)Append a pre-built HealthIssue to the list
addIssueIf(condition, issue)Append an issue only when condition is truthy
requireSystemSecret(name)Add an error issue if the named system secret is missing or empty. Includes a link action to app settings.
requireUserSecret(name, provider?)Add an error issue if the named user secret is missing. Includes an oauth action when a provider is given.
requireAuth(provider)Add an error issue if the user is not authenticated with the given OAuth provider. Includes an oauth action to connect.
requireApiAccess(url, check, options?)Validate connectivity to an external API. check is an async predicate returning true when healthy. Records a timeout or error issue on failure.
build(message?)Compute the final HealthResult from accumulated issues.

requireApiAccess options

interface ApiAccessOptions {
  /** Human-readable label for the API. Default: the URL itself. */
  label?: string;
  /** Timeout in milliseconds. Default: 10000. */
  timeoutMs?: number;
  /** Expected HTTP status codes (for reference in custom predicates). Default: [200, 204]. */
  expectedStatuses?: number[];
  /** OAuth provider to suggest reconnecting when the check fails. */
  provider?: string;
  /** URL to present as a "View API status" link when the check fails. */
  helpUrl?: string;
}

The builder wraps your check predicate with an AbortController timeout. If the predicate returns false or throws (including timeout), the builder records an issue with severity error and an appropriate action — either a link to helpUrl, an OAuth reconnect action for provider, or no action.

Status aggregation rules

The build() method computes the overall status from accumulated issues:

ConditionResulting status
Any issue with severity errornot_ready
Any issue with severity warning (and no error issues)degraded
No issuesready

This means you can chain multiple checks without manually tracking which ones failed — the builder handles the severity ordering for you.


Proactive health updates

In addition to responding to health_check requests from the Station, your app can proactively notify the host when its status changes. This is done by emitting a health_update event through the session transport.

This is useful when your app detects a problem before the next scheduled health check — for example, an API returning 401 during a tool call, or a WebSocket connection dropping unexpectedly.

Proactive health updates are sent at the protocol level via health_update session events. The SDK handles the transport — your app only needs to report issues through the health handler or by calling the appropriate session method.


How the Station uses health checks

The Station uses health information for three key operational decisions:

Monitoring

Health status is displayed in the Station's management UI. When an app reports degraded or not_ready, the Station surfaces the issues — including their code, message, and action — so users can see exactly what's wrong and take corrective action (e.g., configuring a missing secret or reconnecting an OAuth provider).

Restart decisions

When an app reports not_ready, the Station may attempt to restart the instance. The restart behavior depends on the Station's configuration and the app's runtime type. Health checks help the Station distinguish between a transient failure (restart will fix it) and a configuration problem (restart won't help — the user needs to act).

Backpressure

Under load, the Station uses health status to make routing decisions:

  • ready — Tool calls are routed to the instance normally.
  • degraded — The Station may reduce the rate of incoming tool calls or queue them, depending on backpressure configuration.
  • not_ready — The Station stops routing new tool calls to the instance. Pending calls may be queued, retried against another instance, or returned with an error depending on the Station's backpressure policy.

See the Backpressure page for details on how the Station handles traffic under load.


Disabling health checks

If your app doesn't need health checks (e.g., a stateless utility with no external dependencies), you can disable the capability in your app config:

const app = createApp({
  // ...
  capabilities: {
    health: false, // default: true
  },
});

When disabled, the Station will not send health_check requests and the app will always be considered ready.


Complete example

Here's a full example showing an app with multiple health checks using the HealthCheckBuilder:

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

const app = createApp({
  id: 'app.github-issues',
  version: '1.0.0',
  name: 'GitHub Issues',
  description: 'Manage GitHub issues from conversations',
  author: { name: 'Jane Doe', email: 'jane@example.com' },
  category: 'tools',
  runtime: { type: 'docker', image: 'node:22-slim' },
  auth: {
    type: 'oauth2',
    provider: 'github',
    authorizeUrl: 'https://github.com/login/oauth/authorize',
    tokenUrl: 'https://github.com/login/oauth/access_token',
    scopes: ['repo', 'read:org'],
  },
});

app.health(async (ctx) => {
  const builder = new HealthCheckBuilder(ctx.secrets, ctx.auth);

  // Check 1: GitHub OAuth is connected
  await builder.requireAuth('github');

  // Check 2: GitHub API is reachable
  await builder.requireApiAccess(
    'https://api.github.com/zen',
    async (url) => {
      const status = await ctx.auth.getStatus();
      if (!status.authenticated) return false;

      const res = await fetch(url, {
        headers: {
          Accept: 'application/vnd.github+json',
        },
        signal: AbortSignal.timeout(5000),
      });
      return res.ok;
    },
    {
      label: 'GitHub API',
      timeoutMs: 8000,
      helpUrl: 'https://www.githubstatus.com',
    },
  );

  // Check 3: Custom check — verify rate limit headroom
  try {
    const res = await fetch('https://api.github.com/rate_limit', {
      headers: { Accept: 'application/vnd.github+json' },
      signal: AbortSignal.timeout(3000),
    });
    const data = await res.json();
    const remaining = data?.rate?.remaining ?? 0;

    builder.addIssueIf(remaining < 100, {
      code: 'low_rate_limit',
      message: `GitHub API rate limit is low: ${remaining} requests remaining.`,
      severity: 'warning',
    });
  } catch {
    // Rate limit check is best-effort — don't fail the whole health check
    builder.addIssue({
      code: 'rate_limit_check_failed',
      message: 'Could not check GitHub API rate limit status.',
      severity: 'warning',
    });
  }

  return builder.build('GitHub Issues health check completed');
});

export default app.toFactory();

This example demonstrates:

  • requireAuth — verifies the user is connected to GitHub via OAuth
  • requireApiAccess — probes the GitHub API with a timeout and help link
  • addIssueIf — adds a warning when the rate limit is running low
  • addIssue — adds a warning when the rate limit check itself fails
  • build() — aggregates all issues into the correct status automatically

If GitHub OAuth is not connected, the status will be not_ready (error severity). If the API is reachable but the rate limit is low, the status will be degraded (warning severity). If everything is fine, the status will be ready.