diff --git a/frontend/src/app/actor-builds-list.tsx b/frontend/src/app/actor-builds-list.tsx
index 22a66b2e94..ece00a039b 100644
--- a/frontend/src/app/actor-builds-list.tsx
+++ b/frontend/src/app/actor-builds-list.tsx
@@ -1,13 +1,38 @@
-import { faActorsBorderless, Icon } from "@rivet-gg/icons";
+import * as allIcons from "@rivet-gg/icons";
+import { faActorsBorderless, Icon, type IconProp } from "@rivet-gg/icons";
import { useInfiniteQuery } from "@tanstack/react-query";
import { Link, useNavigate } from "@tanstack/react-router";
-import { Fragment } from "react";
+import { Fragment, useMemo } from "react";
import { match } from "ts-pattern";
import { Button, cn, Skeleton } from "@/components";
import { useDataProvider } from "@/components/actors";
import { VisibilitySensor } from "@/components/visibility-sensor";
import { RECORDS_PER_PAGE } from "./data-providers/default-data-provider";
+const emojiRegex =
+ /[\u{1F300}-\u{1F9FF}]|[\u{2600}-\u{26FF}]|[\u{2700}-\u{27BF}]|[\u{1F600}-\u{1F64F}]|[\u{1F680}-\u{1F6FF}]|[\u{1F1E0}-\u{1F1FF}]/u;
+
+function isEmoji(str: string): boolean {
+ return emojiRegex.test(str);
+}
+
+function capitalize(str: string): string {
+ return str.charAt(0).toUpperCase() + str.slice(1);
+}
+
+function toPascalCase(str: string): string {
+ return str
+ .split("-")
+ .map((part) => capitalize(part))
+ .join("");
+}
+
+function lookupFaIcon(iconName: string): IconProp | null {
+ const pascalName = `fa${toPascalCase(iconName)}`;
+ const iconDef = (allIcons as Record)[pascalName];
+ return iconDef ?? null;
+}
+
export function ActorBuildsList() {
const { data, isLoading, hasNextPage, fetchNextPage, isFetchingNextPage } =
useInfiniteQuery(useDataProvider().buildsQueryOptions());
@@ -22,55 +47,79 @@ export function ActorBuildsList() {
Connect RivetKit to see instances.
) : null}
- {data?.map((build) => (
-
+ );
+ })}
{isFetchingNextPage || isLoading
? Array(RECORDS_PER_PAGE)
.fill(null)
diff --git a/rivetkit-typescript/packages/rivetkit/src/actor/config.ts b/rivetkit-typescript/packages/rivetkit/src/actor/config.ts
index 6ecd5884dd..99f1b14b9a 100644
--- a/rivetkit-typescript/packages/rivetkit/src/actor/config.ts
+++ b/rivetkit-typescript/packages/rivetkit/src/actor/config.ts
@@ -21,17 +21,6 @@ import type {
} from "./contexts";
import type { AnyDatabaseProvider } from "./database";
-/**
- * Configuration object that can be returned from `workflow()` to provide
- * metadata and the run handler.
- */
-export interface RunConfig {
- /** Icon to display in the inspector for this run handler */
- icon?: string;
- /** The actual run handler function */
- run: (...args: any[]) => any;
-}
-
export interface ActorTypes<
TState,
TConnParams,
@@ -53,24 +42,49 @@ const zFunction = <
T extends (...args: any[]) => any = (...args: unknown[]) => unknown,
>() => z.custom((val) => typeof val === "function");
+// Schema for run handler with metadata
+export const RunConfigSchema = z.object({
+ /** Display name for the actor in the Inspector UI. */
+ name: z.string().optional(),
+ /** Icon for the actor in the Inspector UI. Can be an emoji or FontAwesome icon name. */
+ icon: z.string().optional(),
+ /** The run handler function. */
+ run: zFunction(),
+});
+export type RunConfig = z.infer;
+
+// Run can be either a function or an object with name/icon/run
+const zRunHandler = z.union([zFunction(), RunConfigSchema]).optional();
+
+/** Extract the run function from either a function or RunConfig object. */
+export function getRunFunction(
+ run: ((...args: any[]) => any) | RunConfig | undefined,
+): ((...args: any[]) => any) | undefined {
+ if (!run) return undefined;
+ if (typeof run === "function") return run;
+ return run.run;
+}
+
+/** Extract run metadata (name/icon) from RunConfig if provided. */
+export function getRunMetadata(
+ run: ((...args: any[]) => any) | RunConfig | undefined,
+): { name?: string; icon?: string } {
+ if (!run || typeof run === "function") return {};
+ return { name: run.name, icon: run.icon };
+}
+
// This schema is used to validate the input at runtime. The generic types are defined below in `ActorConfig`.
//
// We don't use Zod generics with `z.custom` because:
// (a) there seems to be a weird bug in either Zod, tsup, or TSC that causese external packages to have different types from `z.infer` than from within the same package and
// (b) it makes the type definitions incredibly difficult to read as opposed to vanilla TypeScript.
-// Schema for RunConfig objects returned by workflow()
-const RunConfigSchema = z.object({
- icon: z.string().optional(),
- run: zFunction(),
-});
-
export const ActorConfigSchema = z
.object({
onCreate: zFunction().optional(),
onDestroy: zFunction().optional(),
onWake: zFunction().optional(),
onSleep: zFunction().optional(),
- run: z.union([zFunction(), RunConfigSchema]).optional(),
+ run: zRunHandler,
onStateChange: zFunction().optional(),
onBeforeConnect: zFunction().optional(),
onConnect: zFunction().optional(),
@@ -88,6 +102,10 @@ export const ActorConfigSchema = z
createVars: zFunction().optional(),
options: z
.object({
+ /** Display name for the actor in the Inspector UI. */
+ name: z.string().optional(),
+ /** Icon for the actor in the Inspector UI. Can be an emoji or FontAwesome icon name. */
+ icon: z.string().optional(),
createVarsTimeout: z.number().positive().default(5000),
createConnStateTimeout: z.number().positive().default(5000),
onConnectTimeout: z.number().positive().default(5000),
@@ -355,7 +373,7 @@ interface BaseActorConfig<
* On shutdown, the actor waits for this handler to complete with a
* configurable timeout (options.runStopTimeout, default 15s).
*
- * Can be a function or a RunConfig object (returned by `workflow()`).
+ * Can be either a function or a RunConfig object with optional name/icon metadata.
*
* @returns Void or a Promise. If the promise exits, the actor crashes.
*/
@@ -688,6 +706,16 @@ export function test<
export const DocActorOptionsSchema = z
.object({
+ name: z
+ .string()
+ .optional()
+ .describe("Display name for the actor in the Inspector UI."),
+ icon: z
+ .string()
+ .optional()
+ .describe(
+ "Icon for the actor in the Inspector UI. Can be an emoji (e.g., '🚀') or FontAwesome icon name (e.g., 'rocket').",
+ ),
createVarsTimeout: z
.number()
.optional()
diff --git a/rivetkit-typescript/packages/rivetkit/src/actor/instance/mod.ts b/rivetkit-typescript/packages/rivetkit/src/actor/instance/mod.ts
index 7ba4473ced..4bc6623487 100644
--- a/rivetkit-typescript/packages/rivetkit/src/actor/instance/mod.ts
+++ b/rivetkit-typescript/packages/rivetkit/src/actor/instance/mod.ts
@@ -18,7 +18,7 @@ import {
CONN_VERSIONED,
} from "@/schemas/actor-persist/versioned";
import { EXTRA_ERROR_LOG } from "@/utils";
-import type { ActorConfig } from "../config";
+import { getRunFunction, type ActorConfig } from "../config";
import type { ConnDriver } from "../conn/driver";
import { createHttpDriver } from "../conn/drivers/http";
import {
@@ -1169,16 +1169,11 @@ export class ActorInstance {
}
#startRunHandler() {
- if (!this.#config.run) return;
+ const runFn = getRunFunction(this.#config.run);
+ if (!runFn) return;
this.#rLog.debug({ msg: "starting run handler" });
-// Handle both function and RunConfig object (returned by workflow())
- const runFn =
- typeof this.#config.run === "function"
- ? this.#config.run
- : this.#config.run.run;
-
const runSpan = this.startTraceSpan("actor.run");
const runResult = this.#traces.withSpan(runSpan, () =>
runFn(this.actorContext),
diff --git a/rivetkit-typescript/packages/rivetkit/src/registry/config/index.ts b/rivetkit-typescript/packages/rivetkit/src/registry/config/index.ts
index ddb1b3600e..1c0519755f 100644
--- a/rivetkit-typescript/packages/rivetkit/src/registry/config/index.ts
+++ b/rivetkit-typescript/packages/rivetkit/src/registry/config/index.ts
@@ -1,5 +1,6 @@
import { z } from "zod";
import type { ActorDefinition, AnyActorDefinition } from "@/actor/definition";
+import { getRunMetadata } from "@/actor/config";
import { type Logger, LogLevelSchema } from "@/common/log";
import { ENGINE_ENDPOINT } from "@/engine-process/constants";
import { InspectorConfigSchema } from "@/inspector/config";
@@ -265,9 +266,21 @@ export type RegistryConfigInput = Omit<
export function buildActorNames(
config: RegistryConfig,
-): Record }> {
+): Record }> {
return Object.fromEntries(
- Object.keys(config.use).map((name) => [name, { metadata: {} }]),
+ Object.keys(config.use).map((actorName) => {
+ const definition = config.use[actorName];
+ const options = definition.config.options ?? {};
+ const runMeta = getRunMetadata(definition.config.run);
+ const metadata: Record = {};
+ // Actor options take precedence over run metadata
+ metadata.icon = options.icon ?? runMeta.icon;
+ metadata.name = options.name ?? runMeta.name;
+ // Remove undefined values
+ if (!metadata.icon) delete metadata.icon;
+ if (!metadata.name) delete metadata.name;
+ return [actorName, { metadata }];
+ }),
);
}
diff --git a/website/src/content/docs/actors/appearance.mdx b/website/src/content/docs/actors/appearance.mdx
new file mode 100644
index 0000000000..8ecd8c28fe
--- /dev/null
+++ b/website/src/content/docs/actors/appearance.mdx
@@ -0,0 +1,148 @@
+# Icons & Names
+
+Actors can be customized with a display name and icon that appear in the Rivet inspector & dashboard. This helps identify actors at a glance when managing your application.
+
+## Configuration
+
+Set the `name` and `icon` properties in your actor's `options`:
+
+```typescript
+import { actor } from "rivetkit";
+
+const chatRoom = actor({
+ options: {
+ name: "Chat Room", // Human-friendly display name
+ icon: "comments", // FontAwesome icon name
+ },
+ state: { messages: [] },
+ actions: {
+ // ...
+ }
+});
+```
+
+## Icon Formats
+
+The `icon` property accepts two formats:
+
+### Emoji
+
+Use any emoji character directly:
+
+```typescript
+const notificationService = actor({
+ options: {
+ name: "Notifications",
+ icon: "🔔",
+ },
+ // ...
+});
+```
+
+### FontAwesome Icons
+
+Use [FontAwesome](https://fontawesome.com/search) icon names without the "fa" prefix:
+
+```typescript
+const gameServer = actor({
+ options: {
+ name: "Game Server",
+ icon: "gamepad",
+ },
+ // ...
+});
+
+const analyticsWorker = actor({
+ options: {
+ name: "Analytics",
+ icon: "chart-line",
+ },
+ // ...
+});
+```
+
+## Default Behavior
+
+If no `icon` is specified, actors display the default actor icon. If no `name` is specified, the actor's registry key (e.g., `chatRoom`, `gameServer`) is displayed instead.
+
+## Examples
+
+Here are some common patterns:
+
+```typescript
+import { actor } from "rivetkit";
+
+// Chat/messaging actors
+const chatRoom = actor({
+ options: { name: "Chat Room", icon: "comments" },
+ // ...
+});
+
+// Game-related actors
+const matchmaker = actor({
+ options: { name: "Matchmaker", icon: "users" },
+ // ...
+});
+
+const gameSession = actor({
+ options: { name: "Game Session", icon: "gamepad" },
+ // ...
+});
+
+// Data processing actors
+const dataProcessor = actor({
+ options: { name: "Data Processor", icon: "microchip" },
+ // ...
+});
+
+// Using emojis for quick identification
+const alertService = actor({
+ options: { name: "Alerts", icon: "🚨" },
+ // ...
+});
+```
+
+## Advanced: Run Handler Metadata
+
+For library developers creating reusable run handlers, you can bundle icon and name metadata directly with the `run` property. This allows libraries to provide sensible defaults without requiring users to configure them manually.
+
+Instead of returning a function from your run handler factory, return an object with `name`, `icon`, and `run`:
+
+```typescript
+import type { RunConfig } from "rivetkit";
+
+function myCustomRunHandler(options: MyOptions): RunConfig {
+ async function run(c) {
+ // Your run handler logic...
+ }
+
+ return {
+ name: "My Custom Handler",
+ icon: "bolt",
+ run,
+ };
+}
+```
+
+Users can then use this directly:
+
+```typescript
+const myActor = actor({
+ run: myCustomRunHandler({ /* options */ }),
+ // Automatically gets "My Custom Handler" name and "bolt" icon
+});
+```
+
+Actor-level `options.name` and `options.icon` always take precedence, allowing users to override library defaults:
+
+```typescript
+const myActor = actor({
+ options: {
+ name: "Custom Name", // Overrides "My Custom Handler"
+ icon: "rocket", // Overrides "bolt"
+ },
+ run: myCustomRunHandler({ /* options */ }),
+});
+```
+
+The built-in `workflow()` helper uses this pattern to automatically display the workflow icon for workflow-based actors.
diff --git a/website/src/content/docs/actors/index.mdx b/website/src/content/docs/actors/index.mdx
index f96d9abbb4..70783fd8cc 100644
--- a/website/src/content/docs/actors/index.mdx
+++ b/website/src/content/docs/actors/index.mdx
@@ -870,6 +870,22 @@ Find the full client guides here:
- [React Client](/docs/clients/react)
- [Swift Client](/docs/clients/swift)
+### Icons & Names
+
+Customize how actors appear in the UI with display names and icons:
+
+```typescript
+const chatRoom = actor({
+ options: {
+ name: "Chat Room",
+ icon: "💬", // or FontAwesome: "comments", "chart-line", etc.
+ },
+ // ...
+});
+```
+
+[Documentation](/docs/actors/appearance).
+
## Authentication & Security
Validate credentials in `onBeforeConnect` or `createConnState`. Throw an error to reject the connection. Use `c.conn.id` or `c.conn.state` to identify users in actions—never trust user IDs passed as action parameters.
diff --git a/website/src/sitemap/mod.ts b/website/src/sitemap/mod.ts
index 731422859e..4df88a8aa1 100644
--- a/website/src/sitemap/mod.ts
+++ b/website/src/sitemap/mod.ts
@@ -276,6 +276,10 @@ export const sitemap = [
title: "Versions & Upgrades",
href: "/docs/actors/versions",
},
+ {
+ title: "Icons & Names",
+ href: "/docs/actors/appearance",
+ },
],
},
],