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:
| Field | Type | Description |
|---|---|---|
ctx.secrets | SecretsAccess | Read system and user secrets via host callbacks |
ctx.auth | AuthAccess | Check OAuth status, refresh tokens |
ctx.log() | (level, message, data?) => void | Emit 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:
| Field | Type | Required | Description |
|---|---|---|---|
status | 'ready' | 'not_ready' | 'degraded' | Yes | Overall health status |
message | string | No | Human-readable summary shown in the Station UI |
issues | HealthIssue[] | No | Structured issues for detailed UI display |
Status values
| Status | Meaning | Station behavior |
|---|---|---|
ready | All dependencies are healthy | App receives traffic normally |
degraded | Minor issues — app works but with limitations | Station may reduce traffic or show warnings |
not_ready | Critical failure — app cannot serve requests | Station 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
| Method | Description |
|---|---|
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:
| Condition | Resulting status |
|---|---|
Any issue with severity error | not_ready |
Any issue with severity warning (and no error issues) | degraded |
| No issues | ready |
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 OAuthrequireApiAccess— probes the GitHub API with a timeout and help linkaddIssueIf— adds a warning when the rate limit is running lowaddIssue— adds a warning when the rate limit check itself failsbuild()— 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.