Skip to content
This repository was archived by the owner on Oct 22, 2025. It is now read-only.
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
9 changes: 2 additions & 7 deletions examples/ai-agent/src/backend/registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,8 @@ import { openai } from "@ai-sdk/openai";
import { generateText, tool } from "ai";
import { actor, setup } from "rivetkit";
import { z } from "zod";
import { getWeather } from "./my-utils";

export type Message = {
role: "user" | "assistant";
content: string;
timestamp: number;
};
import { getWeather } from "./my-tools";
import type { Message } from "./types";

export const aiAgent = actor({
// Persistent state that survives restarts: https://rivet.dev/docs/actors/state
Expand Down
5 changes: 5 additions & 0 deletions examples/ai-agent/src/backend/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export type Message = {
role: "user" | "assistant";
content: string;
timestamp: number;
};
3 changes: 2 additions & 1 deletion examples/ai-agent/src/frontend/App.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { createRivetKit } from "@rivetkit/react";
import { useEffect, useState } from "react";
import type { Message, registry } from "../backend/registry";
import { registry } from "../backend/registry";
import type { Message } from "../backend/types";

const { useActor } = createRivetKit<typeof registry>("http://localhost:8080");

Expand Down
2 changes: 1 addition & 1 deletion examples/ai-agent/tests/ai-agent.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ vi.mock("ai", () => ({
tool: vi.fn().mockImplementation(({ execute }) => ({ execute })),
}));

vi.mock("../src/backend/my-utils", () => ({
vi.mock("../src/backend/my-tools", () => ({
getWeather: vi.fn().mockResolvedValue({
location: "San Francisco",
temperature: 72,
Expand Down
2 changes: 2 additions & 0 deletions examples/background-jobs/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
.actorcore
node_modules
37 changes: 37 additions & 0 deletions examples/background-jobs/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# Daily Email Campaign for RivetKit

Example project demonstrating scheduled background emails with [RivetKit](https://rivetkit.org).

[Learn More →](https://github.com/rivet-dev/rivetkit)

[Discord](https://rivet.dev/discord) — [Documentation](https://rivetkit.org) — [Issues](https://github.com/rivet-dev/rivetkit/issues)

## Getting Started

### Prerequisites

- Node.js 20+
- [Resend](https://resend.com) API key and verified sender domain

### Installation

```sh
git clone https://github.com/rivet-dev/rivetkit
cd rivetkit/examples/background-jobs
npm install
```

### Development

```sh
RESEND_API_KEY=your-api-key \
RESEND_FROM_EMAIL="Example <hello@example.com>" \
CAMPAIGN_USER_EMAIL=user@example.com \
npm run dev
```

The example creates a single `emailCampaignUser` actor, stores the recipient email, and schedules a daily task that sends mail through the live Resend API. The server logs the next scheduled send time, and the actor reschedules itself after each successful delivery. Set `CAMPAIGN_USER_ID` to control the actor key when you need to track multiple users.

## License

Apache 2.0
22 changes: 22 additions & 0 deletions examples/background-jobs/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
{
"name": "example-background-jobs",
"version": "2.0.14",
"private": true,
"type": "module",
"scripts": {
"dev": "tsx src/server.ts",
"check-types": "tsc --noEmit",
"test": "vitest run"
},
"devDependencies": {
"rivetkit": "workspace:*",
"@types/node": "^22.13.9",
"tsx": "^3.12.7",
"typescript": "^5.7.3",
"vitest": "^3.1.1"
},
"dependencies": {
"resend": "^4.0.1"
},
"stableVersion": "0.8.0"
}
52 changes: 52 additions & 0 deletions examples/background-jobs/src/registry.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { Resend } from "resend";
import { actor, setup } from "rivetkit";
import type { CampaignInput, CampaignState } from "./types";

const DAY_IN_MS = 86_400_000;

const EMAIL_SUBJECT = "Daily campaign update";
const EMAIL_BODY = [
"<p>Hi there,</p>",
"<p>This is your automated daily campaign email from RivetKit.</p>",
"<p>Have a great day!</p>",
].join("");

const emailCampaignUser = actor({
createState: (_c, input: CampaignInput): CampaignState => ({
email: input.email,
}),

onCreate: async (c) => {
const nextSendAt = Date.now() + DAY_IN_MS;
c.state.nextSendAt = nextSendAt;
await c.schedule.at(nextSendAt, "sendDailyEmail");
},

actions: {
sendDailyEmail: async (c) => {
const resend = new Resend(process.env.RESEND_API_KEY ?? "");

const { data, error } = await resend.emails.send({
from: process.env.RESEND_FROM_EMAIL ?? "",
to: c.state.email,
subject: EMAIL_SUBJECT,
html: EMAIL_BODY,
});

c.state.lastSentAt = Date.now();
c.state.lastMessageId = data?.id ?? String(error ?? "");

const nextSendAt = Date.now() + DAY_IN_MS;
c.state.nextSendAt = nextSendAt;
await c.schedule.at(nextSendAt, "sendDailyEmail");
},

getStatus: (c) => c.state,
},
});

export const registry = setup({
use: { emailCampaignUser },
});

export type Registry = typeof registry;
28 changes: 28 additions & 0 deletions examples/background-jobs/src/server.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { registry } from "./registry";

const { client } = registry.start();

async function main() {
const userEmail = process.env.CAMPAIGN_USER_EMAIL;
const userId = process.env.CAMPAIGN_USER_ID ?? "demo-user";

if (!userEmail) {
console.warn(
"Set CAMPAIGN_USER_EMAIL to schedule the daily email campaign (e.g. CAMPAIGN_USER_EMAIL=user@example.com).",
);
return;
}

const campaign = client.emailCampaignUser.getOrCreate(userId, {
createWithInput: { email: userEmail },
});
const status = await campaign.getStatus();

const nextSend = status.nextSendAt
? new Date(status.nextSendAt).toISOString()
: "not scheduled";

console.log(`Next daily email for ${status.email} scheduled at ${nextSend}`);
}

void main();
9 changes: 9 additions & 0 deletions examples/background-jobs/src/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
export type CampaignInput = {
email: string;
};

export type CampaignState = CampaignInput & {
lastSentAt?: number;
lastMessageId?: string;
nextSendAt?: number;
};
43 changes: 43 additions & 0 deletions examples/background-jobs/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
{
"compilerOptions": {
/* Visit https://aka.ms/tsconfig.json to read more about this file */

/* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */
"target": "esnext",
/* Specify a set of bundled library declaration files that describe the target runtime environment. */
"lib": ["esnext"],
/* Specify what JSX code is generated. */
"jsx": "react-jsx",

/* Specify what module code is generated. */
"module": "esnext",
/* Specify how TypeScript looks up a file from a given module specifier. */
"moduleResolution": "bundler",
/* Specify type package names to be included without being referenced in a source file. */
"types": ["node"],
/* Enable importing .json files */
"resolveJsonModule": true,

/* Allow JavaScript files to be a part of your program. Use the `checkJS` option to get errors from these files. */
"allowJs": true,
/* Enable error reporting in type-checked JavaScript files. */
"checkJs": false,

/* Disable emitting files from a compilation. */
"noEmit": true,

/* Ensure that each file can be safely transpiled without relying on other imports. */
"isolatedModules": true,
/* Allow 'import x from y' when a module doesn't have a default export. */
"allowSyntheticDefaultImports": true,
/* Ensure that casing is correct in imports. */
"forceConsistentCasingInFileNames": true,

/* Enable all strict type-checking options. */
"strict": true,

/* Skip type checking all .d.ts files. */
"skipLibCheck": true
},
"include": ["src/**/*.ts", "scripts/**/*.ts", "tests/**/*.ts"]
}
4 changes: 4 additions & 0 deletions examples/background-jobs/turbo.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"$schema": "https://turbo.build/schema.json",
"extends": ["//"]
}
2 changes: 2 additions & 0 deletions examples/bots/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
.actorcore
node_modules
34 changes: 34 additions & 0 deletions examples/bots/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# Discord Bot Gateway for RivetKit

Example project demonstrating Discord Gateway lifecycle management with [RivetKit](https://rivetkit.org).

[Learn More →](https://github.com/rivet-dev/rivetkit)

[Discord](https://rivet.dev/discord) — [Documentation](https://rivetkit.org) — [Issues](https://github.com/rivet-dev/rivetkit/issues)

## Getting Started

### Prerequisites

- Node.js 22+
- Discord application with a bot token and the **Message Content Intent** enabled

### Installation

```sh
git clone https://github.com/rivet-dev/rivetkit
cd rivetkit/examples/bots
npm install
```

### Development

```sh
DISCORD_BOT_TOKEN=your-token npm run dev
```

Invite the bot to a server, then send messages such as "hello" or "ping" in a text channel to see automatic replies and state updates.

## License

Apache 2.0
23 changes: 23 additions & 0 deletions examples/bots/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
{
"name": "example-bots",
"version": "2.0.14",
"private": true,
"type": "module",
"scripts": {
"dev": "tsx src/server.ts",
"check-types": "tsc --noEmit",
"test": "vitest run"
},
"devDependencies": {
"rivetkit": "workspace:*",
"@types/node": "^22.13.9",
"tsx": "^3.12.7",
"typescript": "^5.7.3",
"vitest": "^3.1.1"
},
"dependencies": {
"@hono/node-server": "^1.14.3",
"hono": "^4.7.0"
},
"stableVersion": "0.8.0"
}
62 changes: 62 additions & 0 deletions examples/bots/src/registry.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import { actor, setup } from "rivetkit";
import type { WorkspaceInput, WorkspaceState } from "./types";
import { sendSlackMessage } from "./utils";

const DAY_IN_MS = 86_400_000;

export const slackWorkspaceBot = actor({
createState: async (c, input): Promise<WorkspaceState> => {
// Schedule first daily report
const nextReportAt = Date.now() + DAY_IN_MS;
await c.schedule.at(nextReportAt, "sendDailyReport");

return {
workspaceId: (input as WorkspaceInput).workspaceId,
channelId: (input as WorkspaceInput).channelId,
messageCount: 0,
nextReportAt,
};
},

actions: {
// Called by the Slack webhook in the Hono server
handleMessage: async (c, text: string) => {
c.state.messageCount++;

const msg = text.toLowerCase().trim();
let response: string | undefined;

if (msg === "ping") {
response = "pong";
} else if (msg === "count") {
response = `I've received ${c.state.messageCount} messages in this workspace`;
} else if (msg === "help") {
response =
"Available commands:\n• ping - responds with pong\n• count - shows message count\n• help - shows this message";
}

if (response) {
await sendSlackMessage(c.state.channelId, response);
}
},

sendDailyReport: async (c) => {
// Schedule next report
const nextReportAt = Date.now() + DAY_IN_MS;
c.state.nextReportAt = nextReportAt;
await c.schedule.at(nextReportAt, "sendDailyReport");

// Send report to Slack if we have a channel
if (c.state.channelId) {
const report = `Daily report: ${c.state.messageCount} messages received so far`;
await sendSlackMessage(c.state.channelId, report);
}
},
},
});

export const registry = setup({
use: { slackWorkspaceBot },
});

export type Registry = typeof registry;
Loading
Loading