Actors follow a well-defined lifecycle with hooks at each stage. Understanding these hooks is essential for proper initialization, state management, and cleanup.

Input Parameters

Actors can receive input parameters when created, allowing for flexible initialization:

Defining Input Schema

Use Zod to define a schema for input validation:

import { actor } from "@rivetkit/actor";
import { z } from "zod";

const gameActor = actor({
  createState: (c, opts) => ({
    mode: opts.input?.gameMode ?? "classic",
    maxPlayers: opts.input?.maxPlayers ?? 4,
    difficulty: opts.input?.difficulty ?? "medium",
    players: [],
    status: "waiting",
  }),
  
  actions: {
    // Actions can access input via context
    getGameConfig: (c) => ({
      mode: c.state.mode,
      maxPlayers: c.state.maxPlayers,
      difficulty: c.state.difficulty,
    }),
  },
});

Passing Input to Actors

Input is provided when creating actor instances:

// Client side - create with input
const game = await client.game.create(["game-123"], {
  gameMode: "tournament",
  maxPlayers: 8,
  difficulty: "hard",
});

// getOrCreate can also accept input (used only if creating)
const gameHandle = client.game.getOrCreate(["game-456"], {
  gameMode: "casual",
  maxPlayers: 4,
});

Input in Lifecycle Hooks

Input is available in lifecycle hooks via the opts parameter:

const chatRoom = actor({
  createState: (c, opts) => ({
    name: opts.input?.roomName ?? "Unnamed Room",
    isPrivate: opts.input?.isPrivate ?? false,
    maxUsers: opts.input?.maxUsers ?? 50,
    users: {},
    messages: [],
  }),
  
  onCreate: (c, opts) => {
    console.log(`Creating room: ${opts.input?.roomName}`);
    
    // Setup external services based on input
    if (opts.input?.isPrivate) {
      setupPrivateRoomLogging(opts.input.roomName);
    }
  },
  
  actions: {
    // Input remains accessible in actions
    getRoomInfo: (c) => ({
      name: c.state.name,
      isPrivate: c.state.isPrivate,
      maxUsers: c.state.maxUsers,
      currentUsers: Object.keys(c.state.users).length,
    }),
  },
});

Lifecycle Hooks

Actor lifecycle hooks are defined as functions in the actor configuration.

createState and state

The createState function or state constant defines the initial state of the actor (see state documentation). The createState function is called only once when the actor is first created.

createVars and vars

The createVars function or vars constant defines ephemeral variables for the actor (see state documentation). These variables are not persisted and are useful for storing runtime-only objects or temporary data.

The createVars function can also receive driver-specific context as its second parameter, allowing access to driver capabilities like Rivet KV or Cloudflare Durable Object storage.

import { actor } from "@rivetkit/actor";

// Using vars constant
const counter1 = actor({
  state: { count: 0 },
  vars: { lastAccessTime: 0 },
  actions: { /* ... */ }
});

// Using createVars function
const counter2 = actor({
  state: { count: 0 },
  createVars: () => {
    // Initialize with non-serializable objects
    return { 
      lastAccessTime: Date.now(),
      emitter: new EventTarget() 
    };
  },
  actions: { /* ... */ }
});

// Access driver-specific context
const exampleActor = actor({
  state: { count: 0 },
  // Access driver context in createVars
  createVars: (c, driverCtx) => ({
    driverCtx,
  }),
  actions: {
    doSomething: (c) => {
      // Use driver-specific context
      console.log("Driver context:", c.vars.driverCtx);
    }
  }
});

onCreate

The onCreate hook is called at the same time as createState, but unlike createState, it doesn’t return any value. Use this hook for initialization logic that doesn’t affect the initial state.

import { actor } from "@rivetkit/actor";

// Using state constant
const counter1 = actor({
  state: { count: 0 },
  actions: { /* ... */ }
});

// Using createState function
const counter2 = actor({
  createState: () => {
    // Initialize with a count of 0
    return { count: 0 };
  },
  actions: { /* ... */ }
});

// Using onCreate
const counter3 = actor({
  state: { count: 0 },
  
  // Run initialization logic (logging, external service setup, etc.)
  onCreate: (c, opts) => {
    console.log("Counter actor initialized");
    // Access input parameters if provided
    console.log("Input:", opts.input);
    // Can perform async operations or setup
    // No need to return anything
  },
  
  actions: { /* ... */ }
});

onStart

This hook is called any time the actor is started (e.g. after restarting, upgrading code, or crashing).

This is called after the actor has been initialized but before any connections are accepted.

Use this hook to set up any resources or start any background tasks, such as setInterval.

import { actor } from "@rivetkit/actor";

const counter = actor({
  state: { count: 0 },
  vars: { intervalId: null as NodeJS.Timeout | null },
  
  onStart: (c) => {
    console.log('Actor started with count:', c.state.count);
    
    // Set up interval for automatic counting
    const intervalId = setInterval(() => {
      c.state.count++;
      c.broadcast("countChanged", c.state.count);
      console.log('Auto-increment:', c.state.count);
    }, 10000);
    
    // Store interval ID in vars to clean up later if needed
    c.vars.intervalId = intervalId;
  },
  
  actions: { 
    stop: (c) => {
      if (c.vars.intervalId) {
        clearInterval(c.vars.intervalId);
        c.vars.intervalId = null;
      }
    }
  }
});

onStateChange

Called whenever the actor’s state changes. This is often used to broadcast state updates.

import { actor } from "@rivetkit/actor";

const counter = actor({
  state: { count: 0 },
  
  onStateChange: (c, newState) => {
    // Broadcast the new count to all connected clients
    c.broadcast('countUpdated', {
      count: newState.count
    });
  },
  
  actions: {
    increment: (c) => {
      c.state.count++;
      return c.state.count;
    }
  }
});

createConnState and connState

There are two ways to define the initial state for connections:

  1. connState: Define a constant object that will be used as the initial state for all connections
  2. createConnState: A function that dynamically creates initial connection state based on connection parameters

onBeforeConnect

The onBeforeConnect hook is called whenever a new client connects to the actor. Clients can pass parameters when connecting, accessible via params. This hook is used for connection validation and can throw errors to reject connections.

The onBeforeConnect hook does NOT return connection state - it’s used solely for validation.

import { actor } from "@rivetkit/actor";

const chatRoom = actor({
  state: { messages: [] },
  
  // Method 1: Use a static default connection state
  connState: {
    role: "guest",
    joinTime: 0,
  },
  
  // Method 2: Dynamically create connection state
  createConnState: (c, { params }) => {
    return {
      userId: params.userId || "anonymous",
      role: params.role || "guest",
      joinTime: Date.now()
    };
  },
  
  // Validate connections before accepting them
  onBeforeConnect: (c, { params }) => {
    // Validate authentication
    const authToken = params.authToken;
    if (!authToken || !validateToken(authToken)) {
      throw new Error("Invalid authentication");
    }
    
    // Authentication is valid, connection will proceed
    // The actual connection state will come from connState or createConnState
  },
  
  actions: { /* ... */ }
});

Connections cannot interact with the actor until this method completes successfully. Throwing an error will abort the connection. This can be used for authentication - see Authentication for details.

onConnect

Executed after the client has successfully connected.

import { actor } from "@rivetkit/actor";

const chatRoom = actor({
  state: { users: {}, messages: [] },
  
  onConnect: (c) => {
    // Add user to the room's user list using connection state
    const userId = c.conn.state.userId;
    c.state.users[userId] = {
      online: true,
      lastSeen: Date.now()
    };
    
    // Broadcast that a user joined
    c.broadcast("userJoined", { userId, timestamp: Date.now() });
    
    console.log(`User ${userId} connected`);
  },
  
  actions: { /* ... */ }
});

Messages will not be processed for this actor until this hook succeeds. Errors thrown from this hook will cause the client to disconnect.

onDisconnect

Called when a client disconnects from the actor. Use this to clean up any connection-specific resources.

import { actor } from "@rivetkit/actor";

const chatRoom = actor({
  state: { users: {}, messages: [] },
  
  onDisconnect: (c) => {
    // Update user status when they disconnect
    const userId = c.conn.state.userId;
    if (c.state.users[userId]) {
      c.state.users[userId].online = false;
      c.state.users[userId].lastSeen = Date.now();
    }
    
    // Broadcast that a user left
    c.broadcast("userLeft", { userId, timestamp: Date.now() });
    
    console.log(`User ${userId} disconnected`);
  },
  
  actions: { /* ... */ }
});

Destroying Actors

Actors can be shut down gracefully with c.shutdown(). Clients will be gracefully disconnected.

import { actor } from "@rivetkit/actor";

const temporaryRoom = actor({
  state: { 
    createdAt: 0,
    expiresAfterMs: 3600000 // 1 hour
  },
  
  createState: () => ({
    createdAt: Date.now(),
    expiresAfterMs: 3600000 // 1 hour
  }),
  
  onStart: (c) => {
    // Check if room is expired
    const now = Date.now();
    const expiresAt = c.state.createdAt + c.state.expiresAfterMs;
    
    if (now > expiresAt) {
      console.log("Room expired, shutting down");
      c.shutdown();
    } else {
      // Set up expiration timer
      const timeUntilExpiry = expiresAt - now;
      setTimeout(() => {
        console.log("Room lifetime reached, shutting down");
        c.shutdown();
      }, timeUntilExpiry);
    }
  },
  
  actions: {
    closeRoom: (c) => {
      // Notify all clients
      c.broadcast("roomClosed", { reason: "Admin closed the room" });
      
      // Shutdown the actor
      c.shutdown();
    }
  }
});

This action is permanent and cannot be reverted.

Using ActorContext Type Externally

When extracting logic from lifecycle hooks or actions into external functions, you’ll often need to define the type of the context parameter. RivetKit provides helper types that make it easy to extract and pass these context types to external functions.

import { actor, ActorContextOf } from "@rivetkit/actor";

const myActor = actor({
  state: { count: 0 },
  
  // Use external function in lifecycle hook
  onStart: (c) => logActorStarted(c)
});

// Simple external function with typed context
function logActorStarted(c: ActorContextOf<typeof myActor>) {
  console.log(`Actor started with count: ${c.state.count}`);
}

See Helper Types for more details on using ActorContextOf.

Full Example

import { actor } from "@rivetkit/actor";
import { z } from "zod";

const counter = actor({
  // Initialize state with input
  createState: (c, opts) => ({
    count: opts.input?.initialCount ?? 0,
    stepSize: opts.input?.stepSize ?? 1,
    name: opts.input?.name ?? "Unnamed Counter",
  }),
  
  // Initialize actor (run setup that doesn't affect initial state)
  onCreate: (c, opts) => {
    console.log(`Counter "${opts.input?.name}" initialized`);
    // Set up external resources, logging, etc.
  },
  
  // Define default connection state
  connState: {
    role: "guest"
  },
  
  // Dynamically create connection state based on params
  createConnState: (c, { params }) => {
    // Get auth info from validation
    const authToken = params.authToken;
    const authInfo = validateAuthToken(authToken);
    
    return {
      userId: authInfo?.userId || "anonymous",
      role: authInfo?.role || "guest"
    };
  },
  
  // Lifecycle hooks
  onStart: (c) => {
    console.log(`Counter "${c.state.name}" started with count:`, c.state.count);
  },
  
  onStateChange: (c, newState) => {
    c.broadcast('countUpdated', { 
      count: newState.count,
      name: newState.name 
    });
  },
  
  onBeforeConnect: (c, { params }) => {
    // Validate auth token
    const authToken = params.authToken;
    if (!authToken) {
      throw new Error('Missing auth token');
    }
    
    // Validate with your API and determine the user
    const authInfo = validateAuthToken(authToken);
    if (!authInfo) {
      throw new Error('Invalid auth token');
    }
    
    // If validation succeeds, connection proceeds
    // Connection state will be set by createConnState
  },
  
  onConnect: (c) => {
    console.log(`User ${c.conn.state.userId} connected to "${c.state.name}"`);
  },
  
  onDisconnect: (c) => {
    console.log(`User ${c.conn.state.userId} disconnected from "${c.state.name}"`);
  },
  
  // Define actions
  actions: {
    increment: (c, amount?: number) => {
      const step = amount ?? c.state.stepSize;
      c.state.count += step;
      return c.state.count;
    },
    
    reset: (c) => {
      // Check if user has admin role
      if (c.conn.state.role !== "admin") {
        throw new Error("Unauthorized: requires admin role");
      }
      
      c.state.count = 0;
      return c.state.count;
    },
    
    getInfo: (c) => ({
      name: c.state.name,
      count: c.state.count,
      stepSize: c.state.stepSize,
    }),
  }
});

export default counter;