Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions bot_update_scheduled_bots/.env.sample
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
RECALL_API_KEY=RECALL_API_KEY
RECALL_REGION=RECALL_REGION # e.g. us-west-2, us-east-1, eu-central-1, ap-northeast-1
96 changes: 96 additions & 0 deletions bot_update_scheduled_bots/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
# Update scheduled bots

This example demonstrates how to bulk update scheduled bots using the Recall.ai API.

A **scheduled bot** is a bot that hasn't joined a meeting yet — the current time is still less than the bot's `join_at` timestamp.

## Pre-requisites

- [Node.js](https://nodejs.org/en/download)
- [NPM](https://docs.npmjs.com/downloading-and-installing-node-js-and-npm)

## Quickstart

### 1. Set up environment variables

Copy the `.env.sample` file and rename it to `.env`:

```bash
cp .env.sample .env
```

Then fill out the variables in the `.env` file:

- `RECALL_API_KEY` - Your Recall.ai API key
- `RECALL_REGION` - Your Recall.ai region (e.g., `us-west-2`)

### 2. Install dependencies

Open this directory in a terminal and run:

```bash
npm install
```

### 3. Run the script

Update all scheduled bots starting from a future date:

```bash
npx ts-node src/index.ts \
--start_date_utc "2025-12-15 00:00:00" \
--update_data '{"bot_name":"Updated Bot"}'
```

Update scheduled bots within a date range:

```bash
npx ts-node src/index.ts \
--start_date_utc "2025-12-15 00:00:00" \
--end_date_utc "2025-12-31 00:00:00" \
--update_data '{"meeting_url":"https://new-meeting.example.com"}'
```

Filter by custom metadata to update only specific customer's bots:

```bash
npx ts-node src/index.ts \
--start_date_utc "2025-12-15 00:00:00" \
--metadata '{"team_id":"1872"}' \
--update_data '{"bot_name":"Team Bot"}'
```

Update recording retention to 168 hours (7 days):

```bash
npx ts-node src/index.ts \
--start_date_utc "2025-12-15 00:00:00" \
--update_data '{"recording_config":{"retention":{"type":"timed","hours":168}}}'
```

### 4. View the output

The script will output progress and final count:

```
Updating scheduled bots: 2025-12-15 00:00:00 → 2025-12-31 00:00:00

Update data: {"bot_name":"Updated Bot"}

{ pageCount: 5, nextPage: null }
Updated bot: abc123
Updated bot: def456
...

Updated 5 bots
```

## CLI Options

| Option | Required | Description |
| ------------------ | -------- | --------------------------------------------------------------------------- |
| `--start_date_utc` | Yes | Update bots scheduled to join after this date (must be in the future) |
| `--end_date_utc` | No | Update bots scheduled to join before this date |
| `--metadata` | No | JSON object to filter by custom bot metadata (e.g., `'{"team_id":"1872"}'`) |
| `--update_data` | Yes | JSON object with fields to update on each bot |
| `--help` | No | Show help message |
22 changes: 22 additions & 0 deletions bot_update_scheduled_bots/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
{
"name": "bot_update_scheduled_bots",
"version": "1.0.0",
"description": "Update scheduled bots",
"main": "index.ts",
"scripts": {
"dev": "ts-node src/index.ts"
},
"author": "Gerry Saporito",
"license": "MIT",
"devDependencies": {
"@types/mri": "^1.1.4",
"@types/node": "^24.10.1",
"ts-node": "^10.9.2",
"typescript": "^5.9.3"
},
"dependencies": {
"dotenv": "^17.2.3",
"mri": "^1.2.0",
"zod": "^4.1.13"
}
}
105 changes: 105 additions & 0 deletions bot_update_scheduled_bots/src/bot_update_scheduled_bots.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
import { z } from "zod";
import { env } from "./config/env";
import { fetch_with_retry } from "./fetch_with_retry";
import { BotArtifactSchema } from "./schemas/BotArtifactSchema";

/**
* Update scheduled bots after a given date.
*/
export async function bot_update_scheduled_bots(args: any) {
const { start_date_utc, end_date_utc, metadata, update_data } = z.object({
start_date_utc: z.string(),
end_date_utc: z.string().optional(),
metadata: z.record(z.string(), z.string()).optional(),
update_data: z.record(z.string(), z.unknown()),
}).parse(args);

let count = 0;
let next: string | null = null;
do {
const page = await list_bots({
join_at_after: start_date_utc,
join_at_before: end_date_utc,
metadata,
next,
});
console.log({ pageCount: page.results.length, nextPage: page.next });

await Promise.all(page.results.map(async (bot) => {
await update_scheduled_bot_by_id({ bot_id: bot.id, update_data });
console.log(`Updated bot: ${bot.id}`);
}));
count += page.results.length;
next = page.next;
} while (next);

return { count };
}

/**
* Filters bots by the given arguments.
* Returns a page of bots and the next page URL to fetch the next page of bots.
*/
async function list_bots(args: {
next?: string | null; // next page URL
join_at_after?: string; // ISO 8601, e.g. "2025-12-15 00:00:00"
join_at_before?: string; // ISO 8601, e.g. "2025-12-15 00:25:00"
metadata?: Record<string, string>; // add one key-value pair
}) {
const { next, join_at_after, join_at_before, metadata } = z.object({
next: z.string().nullable(),
join_at_after: z.string().optional(),
join_at_before: z.string().optional(),
metadata: z.record(z.string(), z.string()).optional(),
}).parse(args);

const url = next
? new URL(next)
: new URL(`https://${env.RECALL_REGION}.recall.ai/api/v1/bot`);
if (!next) {
if (join_at_after) url.searchParams.set("join_at_after", join_at_after);
if (join_at_before) url.searchParams.set("join_at_before", join_at_before);
if (metadata) {
for (const [key, value] of Object.entries(metadata)) {
url.searchParams.set(`metadata__${key}`, value);
}
}
}

const response = await fetch_with_retry(url.toString(), {
method: "GET",
headers: {
"Authorization": `${env.RECALL_API_KEY}`,
"Content-Type": "application/json",
},
});
if (!response.ok) throw new Error(await response.text());

return z.object({
results: BotArtifactSchema.array(),
next: z.string().nullable(),
}).parse(await response.json());
}

/**
* Updates a bot by its ID.
*/
async function update_scheduled_bot_by_id(args: {
bot_id: string;
update_data: Record<string, unknown>;
}) {
const { bot_id, update_data } = z.object({
bot_id: z.string(),
update_data: z.record(z.string(), z.unknown()),
}).parse(args);

const response = await fetch_with_retry(`https://${env.RECALL_REGION}.recall.ai/api/v1/bot/${bot_id}/`, {
method: "PATCH",
headers: {
"Authorization": `${env.RECALL_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify(update_data),
});
if (!response.ok) throw new Error(await response.text());
}
6 changes: 6 additions & 0 deletions bot_update_scheduled_bots/src/config/env.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import dotenv from "dotenv";
import { EnvSchema } from "../schemas/EnvSchema";

dotenv.config();

export const env = EnvSchema.parse(process.env);
24 changes: 24 additions & 0 deletions bot_update_scheduled_bots/src/fetch_with_retry.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
/**
* Helper function to fetch with retry.
* Respects the Retry-After header.
*/
export async function fetch_with_retry(url: string, options: RequestInit, max_attempts: number = 5): Promise<Response> {
for (let attempt = 1; attempt <= max_attempts; attempt++) {
const response = await fetch(url, options);
if (response.status === 429) {
let retry_after = Number(response.headers.get("Retry-After")) || 0;
console.log(`Rate limit exceeded, retrying in ${retry_after} seconds`);
if (!retry_after) {
console.error("Retry-After header not found");
retry_after = 0;
}
await new Promise((resolve) => setTimeout(
resolve,
1000 * (retry_after + Math.ceil(Math.random() * 5)),
));
continue;
}
return response;
}
throw new Error(`Max attempts (${max_attempts}) reached while fetching ${url}. options=${JSON.stringify(options)}`);
}
56 changes: 56 additions & 0 deletions bot_update_scheduled_bots/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import mri from "mri";
import { bot_update_scheduled_bots } from "./bot_update_scheduled_bots";
import { CmdLineArgsSchema } from "./schemas/CmdLineArgsSchema";

async function main() {
const raw = mri(process.argv.slice(2), { alias: { h: "help" } });

if (raw.help) {
console.log(`
Usage: npx ts-node src/index.ts [options]

Options:
--start_date_utc Update bots scheduled to join after this date (ISO 8601, e.g., "2025-01-01 00:00:00")
--end_date_utc Update bots scheduled to join before this date (ISO 8601, e.g., "2025-02-01 00:00:00") [optional]
--metadata Filter by custom metadata (e.g., '{"customer_id":"123"}')
--update_data JSON object with fields to update (e.g., '{"bot_name":"New Name"}')
--help Show this help message

Examples:
npx ts-node src/index.ts \\
--start_date_utc "2025-12-15 00:00:00" \\
--update_data '{"bot_name":"Updated Bot"}'

npx ts-node src/index.ts \\
--start_date_utc "2025-12-15 00:00:00" \\
--end_date_utc "2025-12-31 00:00:00" \\
--update_data '{"meeting_url":"https://new-meeting.example.com"}'

npx ts-node src/index.ts \\
--start_date_utc "2025-12-15 00:00:00" \\
--metadata '{"team":"engineering"}' \\
--update_data '{"bot_name":"Eng Bot"}'

npx ts-node src/index.ts \\
--start_date_utc "2025-12-15 00:00:00" \\
--update_data '{"recording_config":{"retention":{"type":"timed","hours":168}}}'
`);
process.exit(0);
}

const args = CmdLineArgsSchema.parse(raw);

console.log(`Updating scheduled bots: ${args.start_date_utc}${args.end_date_utc ? ` → ${args.end_date_utc}` : ""}\n`);
console.log(`Update data: ${JSON.stringify(args.update_data)}\n`);

try {
const { count } = await bot_update_scheduled_bots(args);
console.log(`\nUpdated ${count} bots`);
} catch (error) {
console.error("Error updating scheduled bots:");
console.error(error);
process.exit(1);
}
}

main().catch((e) => { console.error("Error:", e.message); process.exit(1); });
9 changes: 9 additions & 0 deletions bot_update_scheduled_bots/src/schemas/BotArtifactSchema.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { z } from "zod";

export const BotArtifactSchema = z.object({
id: z.string(),
status_changes: z.array(z.object({
code: z.string(), // status code, e.g. "joining_call", "done", "fatal"
created_at: z.string(), // ISO 8601, e.g. "2025-12-15 00:00:00"
})),
});
12 changes: 12 additions & 0 deletions bot_update_scheduled_bots/src/schemas/CmdLineArgsSchema.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { z } from "zod";

export const CmdLineArgsSchema = z.object({
start_date_utc: z.string({ message: "--start_date_utc is required" }).refine(
(date) => new Date(date) > new Date(new Date().getTime() - 1 * 60 * 1000), // start date must be greater than 1 minute ago
{ message: "--start_date_utc must be in the future" },
),
end_date_utc: z.string().optional(),
metadata: z.string().optional().transform((v) => v ? JSON.parse(v) : {}),
update_data: z.string({ message: "--update_data is required" }).transform((v) => JSON.parse(v)),
help: z.boolean().optional(),
});
6 changes: 6 additions & 0 deletions bot_update_scheduled_bots/src/schemas/EnvSchema.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { z } from "zod";

export const EnvSchema = z.object({
RECALL_API_KEY: z.string(),
RECALL_REGION: z.string(),
});
14 changes: 14 additions & 0 deletions bot_update_scheduled_bots/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{
"compilerOptions": {
"target": "ES2022",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"outDir": "dist",
"rootDir": "src",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"sourceMap": true
},
"include": ["src"]
}
Loading