Actor state provides the best of both worlds: it’s stored in-memory and persisted automatically. This lets you work with the data without added latency while still being able to survive crashes & upgrades.

Using External SQL Databases

Actors can also be used with external SQL databases. This can be useful to integrate actors with existing applications or for storing relational data. Read more here.

Initializing State

There are two ways to define an actor’s initial state:

Method 1: Static Initial State

import { actor } from "actor-core";

// Simple state with a constant
const counter = actor({
  // Define state as a constant
  state: { count: 0 },
  
  actions: {
    // ...
  }
});

Method 2: Dynamic Initial State

import { actor } from "actor-core";

// State with initialization logic
const counter = actor({
  // Define state using a creation function
  createState: () => {
    return { count: 0 };
  },
  
  actions: {
    // ...
  }
});

The createState function is called once when the actor is first created. See Lifecycle for more details.

Modifying State

To update state, modify the state property on the context object (c.state) in your actions:

import { actor } from "actor-core";

const counter = actor({
  state: { count: 0 },
  
  actions: {
    // Define action to update state
    increment: (c) => {
      // Update state, this will automatically be persisted
      c.state.count += 1;
      return c.state.count;
    },
    
    add: (c, value) => {
      c.state.count += value;
      return c.state.count;
    }
  }
});

Only state stored in the state object will be persisted. Any other variables or properties outside of this are not persisted.

State Saves

Actors automatically handle persisting state transparently. This happens at the end of every action if the state has changed.

In the rare occasion you need to force a state change mid-action, you can use c.saveState(). This should only be used if your action makes an important state change that needs to be persisted before the action completes.

import { actor } from "actor-core";

const criticalProcess = actor({
  state: { 
    steps: [],
    currentStep: 0
  },
  
  actions: {
    processStep: async (c) => {
      // Update to current step
      c.state.currentStep += 1;
      c.state.steps.push(`Started step ${c.state.currentStep}`);
      
      // Force save state before the async operation
      c.saveState();
      
      // Long-running operation that might fail
      await someRiskyOperation();
      
      // Update state again
      c.state.steps.push(`Completed step ${c.state.currentStep}`);
      
      return c.state.currentStep;
    }
  }
});

State Isolation

Each actor’s state is completely isolated, meaning it cannot be accessed directly by other actors or clients. This allows actors to maintain a high level of security and data integrity, ensuring that state changes are controlled and predictable.

To interact with an actor’s state, you must use Actions. Actions provide a controlled way to read from and write to the state.

Sharing State Between Actors

If you need a shared state between multiple actors, you have two options:

  1. Create an actor that holds the shared state that other actors can make action calls to
  2. Use an external database, see External SQL Databases

Ephemeral Variables

In addition to persisted state, ActorCore provides a way to store ephemeral data that is not saved to permanent storage using vars. This is useful for temporary data that only needs to exist while the actor is running or data that cannot be serialized.

vars is designed to complement state, not replace it. Most actors should use both: state for critical business data and vars for ephemeral or non-serializable data.

Initializing Variables

There are two ways to define an actor’s initial vars:

Method 1: Static Initial Variables

import { actor } from "actor-core";

// Define vars as a constant
const counter = actor({
  state: { count: 0 },
  
  // Define ephemeral variables
  vars: { 
    lastAccessTime: 0,
    emitter: createNanoEvents()
  },
  
  actions: {
    // ...
  }
});

When using static vars, all values must be compatible with structuredClone(). If you need to use non-serializable objects, use createVars instead, which allows you to create these objects on the fly.

Method 2: Dynamic Initial Variables

import { actor } from "actor-core";

// Define vars with initialization logic
const counter = actor({
  state: { count: 0 },
  
  // Define vars using a creation function
  createVars: () => {
    return { 
      lastAccessTime: Date.now(),
      emitter: createNanoEvents()
    };
  },
  
  actions: {
    // ...
  }
});

Using Variables

Vars can be accessed and modified through the context object with c.vars:

import { actor } from "actor-core";
import { createNanoEvents } from "nanoevents";

const counter = actor({
  // Persistent state - saved to storage
  state: { count: 0 },
  
  // Create ephemeral objects that won't be serialized
  createVars: () => {
    // Create an event emitter (can't be serialized)
    const emitter = createNanoEvents();
    
    // Set up event listener directly in createVars
    emitter.on('count-changed', (newCount) => {
      console.log(`Count changed to: ${newCount}`);
    });
    
    return { emitter };
  },
  
  actions: {
    increment: (c) => {
      // Update persistent state
      c.state.count += 1;
      
      // Use non-serializable emitter
      c.vars.emitter.emit('count-changed', c.state.count);
      
      return c.state.count;
    }
  }
});

When to Use vars vs state

In practice, most actors will use both: state for critical business data and vars for ephemeral, non-serializable, or performance-sensitive data.

Use vars when:

  • You need to store temporary data that doesn’t need to survive restarts
  • You need to maintain runtime-only references that can’t be serialized (database connections, event emitters, class instances, etc.)

Use state when:

  • The data must be preserved across actor sleeps, restarts, updates, or crashes
  • The information is essential to the actor’s core functionality and business logic

Limitations

State is constrained to the available memory.

Only JSON-serializable types can be stored in state. In serverless runtimes that support it (Rivet, Cloudflare Workers), state is persisted under the hood in a compact, binary format. This is because JavaScript classes cannot be serialized & deserialized.