March 30, 2026 · 8 min read

Trigger the Sahha Health Agent Automatically

Take the Sahha health agent from tutorial four and trigger it automatically — from a Sahha webhook when new scores arrive, or on a nightly cron for a cohort of profiles.

What you are building

The previous tutorial built a health AI agent as a Node.js script you run on demand. This tutorial turns it into a server that runs the agent automatically.

You will build two trigger patterns on top of the same agent logic:

Pattern 1 — Webhook trigger: Sahha calls your endpoint every time it generates new scores for a profile. Your server receives the event, runs the agent for that profile, and stores the result.

Pattern 2 — Cron trigger: A scheduled job runs nightly, loops through a list of profile IDs, runs the agent for each one, and stores the results.

Pattern 1: Sahha → webhook event → your Express server → agent → stored insight
Pattern 2: Cron schedule → your Express server → agent (per profile) → stored insights

Both patterns use the same runHealthAgent function from the previous tutorial. The only thing that changes is what starts the agent running.

Prerequisites

This tutorial builds directly on Build a Health AI Agent with the Sahha MCP Server. You will need:

  • The agent project from that tutorial with runHealthAgent working
  • A Sahha webhook configured (set one up here)
  • A way to expose your local server to the internet during development — ngrok works well

1. Add Express and a scheduler

In your agent project, install the additional dependencies:

npm install express node-cron
npm install -D @types/express @types/node-cron
  • express — HTTP server to receive webhook events
  • node-cron — lightweight cron scheduler for the nightly pattern

2. Extract the agent into a shared module

If runHealthAgent is currently defined inside src/agent.ts alongside the run script, move it into its own module so both trigger patterns can import it.

Create src/run-agent.ts:

// src/run-agent.ts
// Re-export runHealthAgent so both webhook and cron triggers can use it.

export { runHealthAgent } from './agent.js'

Or if you prefer, move the function directly into a dedicated file and update src/agent.ts to import from it. Either approach works — the key is that runHealthAgent is importable without also running the CLI entrypoint code.

3. Build the server

Create src/server.ts. This sets up the Express server with both trigger patterns.

// src/server.ts

import express from 'express'
import cron from 'node-cron'
import { runHealthAgent } from './run-agent.js'

const app = express()
app.use(express.json())

// ---------------------------------------------------------------------------
// Simple in-memory insight store.
// In production, replace this with your database of choice.
// ---------------------------------------------------------------------------
const insightStore = new Map<string, unknown>()

// ---------------------------------------------------------------------------
// Pattern 1: Webhook trigger
//
// Sahha calls this endpoint when new scores are generated for a profile.
// The webhook payload includes the externalId of the profile.
// ---------------------------------------------------------------------------
app.post('/webhooks/sahha', async (req, res) => {
  // Acknowledge the webhook immediately — Sahha expects a fast 200 response.
  // The agent runs asynchronously after the response is sent.
  res.status(200).json({ ok: true })

  const payload = req.body

  // Extract the externalId from the Sahha webhook payload.
  // Adjust this field path to match your webhook event shape.
  const externalId: string | undefined = payload?.externalId ?? payload?.profileId

  if (!externalId) {
    console.warn('Webhook received with no externalId — skipping agent run')
    return
  }

  // Only run the agent for score events — ignore other event types if needed.
  // Remove this check if you want to trigger on all Sahha webhook events.
  const eventType: string | undefined = payload?.type
  if (eventType && !eventType.includes('score')) {
    console.log(`Skipping agent for event type: ${eventType}`)
    return
  }

  console.log(`Webhook received for profile: ${externalId}. Running agent...`)

  try {
    const insight = await runHealthAgent(externalId)
    insightStore.set(externalId, insight)
    console.log(`Agent complete for profile: ${externalId}`)
  } catch (error) {
    console.error(`Agent failed for profile ${externalId}:`, error)
  }
})

// ---------------------------------------------------------------------------
// Pattern 2: Cron trigger
//
// Runs nightly at midnight for a fixed list of profile IDs.
// In production, replace PROFILE_IDS with a database query.
// ---------------------------------------------------------------------------
const PROFILE_IDS = (process.env.PROFILE_IDS ?? '').split(',').filter(Boolean)

cron.schedule('0 0 * * *', async () => {
  if (PROFILE_IDS.length === 0) {
    console.log('Cron: no profiles configured — set PROFILE_IDS in your environment')
    return
  }

  console.log(`Cron: running agent for ${PROFILE_IDS.length} profiles...`)

  for (const externalId of PROFILE_IDS) {
    try {
      console.log(`  → Running agent for: ${externalId}`)
      const insight = await runHealthAgent(externalId)
      insightStore.set(externalId, insight)
      console.log(`  ✓ Complete: ${externalId}`)
    } catch (error) {
      console.error(`  ✗ Failed for ${externalId}:`, error)
      // Continue to the next profile even if one fails
    }
  }

  console.log('Cron: all profiles processed')
})

// ---------------------------------------------------------------------------
// Utility: retrieve a stored insight by profile ID
// ---------------------------------------------------------------------------
app.get('/insights/:externalId', (req, res) => {
  const insight = insightStore.get(req.params.externalId)

  if (!insight) {
    return res.status(404).json({ error: 'No insight found for this profile' })
  }

  return res.json(insight)
})

// Health check
app.get('/health', (_req, res) => {
  res.json({ ok: true, profiles: insightStore.size })
})

// ---------------------------------------------------------------------------
// Start
// ---------------------------------------------------------------------------
const PORT = process.env.PORT ?? 3000

app.listen(PORT, () => {
  console.log(`Sahha health agent server listening on port ${PORT}`)

  if (PROFILE_IDS.length > 0) {
    console.log(`Cron scheduled for ${PROFILE_IDS.length} profiles: ${PROFILE_IDS.join(', ')}`)
  } else {
    console.log('Cron: no PROFILE_IDS set — webhook trigger only')
  }
})

4. Update your environment

Add the profile list for the cron trigger to your .env:

ANTHROPIC_API_KEY=your-anthropic-api-key
SAHHA_MCP_URL=https://your-railway-url.up.railway.app/mcp
SAHHA_MCP_TOKEN=your-mcp-bearer-token
PROFILE_IDS=user-123,user-456,user-789
PORT=3000

PROFILE_IDS is a comma-separated list of Sahha externalId values to process on the nightly cron. Leave it empty if you only want webhook-triggered runs.

5. Update package.json

Add a start script for the server:

{
  "scripts": {
    "build": "tsc",
    "start": "node dist/server.js",
    "start:agent": "node dist/agent.js",
    "dev": "tsx src/server.ts",
    "dev:agent": "tsx src/agent.ts"
  }
}

6. Test locally with the webhook trigger

Start the server:

npm run dev

In a separate terminal, expose it with ngrok:

ngrok http 3000

Copy the ngrok URL (e.g. https://abc123.ngrok.io) and register it as a Sahha webhook endpoint in your Sahha dashboard:

https://abc123.ngrok.io/webhooks/sahha

Trigger a test event from the Sahha dashboard or use the Demo App to generate new scores. You should see the agent run in your terminal:

Webhook received for profile: user-123. Running agent...
  → Calling tool: get_scores ...
  ← Result: [{"type":"sleep"...
  → Calling tool: get_biomarkers ...
Agent complete for profile: user-123

Retrieve the stored insight:

curl http://localhost:3000/insights/user-123

7. Test the cron trigger manually

The cron runs at midnight, which is not ideal for testing. You can trigger a manual run by adding a test endpoint temporarily:

// Add to src/server.ts during development — remove before deploying
app.post('/run-cron-now', async (_req, res) => {
  res.json({ ok: true, message: 'Cron running in background' })

  for (const externalId of PROFILE_IDS) {
    try {
      const insight = await runHealthAgent(externalId)
      insightStore.set(externalId, insight)
    } catch (error) {
      console.error(`Failed for ${externalId}:`, error)
    }
  }
})

Trigger it:

curl -X POST http://localhost:3000/run-cron-now

Deploy to Railway

The server deploys to Railway the same way as the MCP server from tutorial three.

railway init
railway variables set ANTHROPIC_API_KEY=your-key
railway variables set SAHHA_MCP_URL=https://your-mcp-server.up.railway.app/mcp
railway variables set SAHHA_MCP_TOKEN=your-token
railway variables set PROFILE_IDS=user-123,user-456
railway up

Once deployed, update your Sahha webhook URL from the ngrok URL to your Railway URL:

https://your-agent-server.up.railway.app/webhooks/sahha

:::info Persistent storage The in-memory insightStore in this tutorial resets every time the server restarts. For production, replace it with a database — Supabase, Firebase, or Postgres on Railway all work well with this pattern. :::

Choosing between webhook and cron

Both patterns use the same agent. The choice comes down to your use case:

WebhookCron
When it runsAs soon as Sahha generates new dataOn a fixed schedule
Best forReal-time, per-user experiencesNightly digests, batch processing
Profile scopeOne profile per triggerMany profiles per run
LatencyLow — runs within seconds of new dataFixed — runs at scheduled time

Many production apps use both: webhooks for real-time triggers on active users, and a nightly cron as a catch-up pass for profiles that did not trigger a webhook that day.

What to build next

You now have a complete, production-ready Sahha health agent pipeline:

  1. MCP server exposing Sahha tools
  2. Remote deployment on Railway
  3. Agent querying and analysing health data
  4. Automated triggers via webhook and cron

From here, natural extensions include storing insights in a database, sending the output to a notification platform like OneSignal or Braze, or building a UI that displays the structured insight for each user.