Daydreams Logo
Concepts

Actions

Define capabilities and interactions for your Daydreams agent.

What is an Action?

An action is something your agent can do - like calling an API, saving data, or performing calculations. Think of actions as giving your agent superpowers.

Real Examples

Here are actions that make agents useful:

Weather Action

weather-action.ts
// Agent can check weather
const getWeather = action({
  name: "get-weather",
  description: "Gets current weather for a city",
  schema: z.object({
    city: z.string(),
  }),
  handler: async ({ city }) => {
    const response = await fetch(`https://api.weather.com/${city}`);
    return await response.json(); // { temperature: "72°F", condition: "sunny" }
  },
});

Database Action

database-action.ts
// Agent can save user preferences
const savePreference = action({
  name: "save-preference",
  description: "Saves a user preference",
  schema: z.object({
    userId: z.string(),
    key: z.string(),
    value: z.string(),
  }),
  handler: async ({ userId, key, value }) => {
    await database.save(userId, key, value);
    return { success: true, message: "Preference saved!" };
  },
});

Email Action

email-action.ts
// Agent can send emails
const sendEmail = action({
  name: "send-email",
  description: "Sends an email to a user",
  schema: z.object({
    to: z.string(),
    subject: z.string(),
    body: z.string(),
  }),
  handler: async ({ to, subject, body }) => {
    await emailService.send({ to, subject, body });
    return { sent: true, timestamp: new Date().toISOString() };
  },
});

The Problem Without Actions

Without actions, your agent can only talk:

limited-agent.txt
User: "What's the weather in Boston?"
Agent: "I don't have access to weather data, but I imagine it might be nice!"
// ❌ Can't actually check weather
// ❌ Just makes stuff up
// ❌ Not helpful

The Solution: Actions Give Agents Capabilities

With actions, your agent can do things:

capable-agent.txt
User: "What's the weather in Boston?"
Agent: *calls getWeather action*
Agent: "It's 65°F and cloudy in Boston right now!"
// ✅ Actually checks real weather API
// ✅ Provides accurate information
// ✅ Actually useful

How Actions Work in Your Agent

1. You Define What the Agent Can Do

define-actions.ts
const agent = createDreams({
  model: openai("gpt-4o"),
  actions: [
    getWeather, // Agent can check weather
    savePreference, // Agent can save user data
    sendEmail, // Agent can send emails
  ],
});

2. The LLM Decides When to Use Them

When the agent thinks, it sees:

llm-prompt.txt
Available actions:
- get-weather: Gets current weather for a city
- save-preference: Saves a user preference
- send-email: Sends an email to a user

User message: "Check weather in NYC and email it to john@example.com"

3. The LLM Calls Actions

The LLM responds with structured calls:

llm-response.xml
<response>
  <reasoning>I need to check weather first, then email the results</reasoning>
 
  <action_call name="get-weather">{"city": "NYC"}</action_call>
  <action_call name="send-email">{
    "to": "john@example.com",
    "subject": "NYC Weather",
    "body": "Current weather: {{calls[0].temperature}}, {{calls[0].condition}}"
  }</action_call>
</response>

4. Daydreams Executes Your Code

Daydreams automatically:

  • Validates the arguments against your schema
  • Runs your handler function
  • Returns results to the LLM
  • Handles errors gracefully

Creating Your First Action

Here's a simple action that adds two numbers:

calculator-action.ts
import { action } from "@daydreamsai/core";
import { z } from "zod";
 
export const addNumbers = action({
  // Name the LLM uses to call this action
  name: "add-numbers",
 
  // Description helps LLM know when to use it
  description: "Adds two numbers together",
 
  // Schema defines what arguments are required
  schema: z.object({
    a: z.number().describe("First number"),
    b: z.number().describe("Second number"),
  }),
 
  // Handler is your actual code that runs
  handler: async ({ a, b }) => {
    const result = a + b;
    return {
      sum: result,
      message: `${a} + ${b} = ${result}`,
    };
  },
});

Use it in your agent:

agent-with-calculator.ts
const agent = createDreams({
  model: openai("gpt-4o"),
  actions: [addNumbers],
});
 
// Now when user asks "What's 5 + 3?", the agent will:
// 1. Call addNumbers action with {a: 5, b: 3}
// 2. Get back {sum: 8, message: "5 + 3 = 8"}
// 3. Respond: "5 + 3 = 8"

Working with State and Memory

Actions can read and modify your agent's memory:

todo-actions.ts
// Define what your context remembers
interface TodoMemory {
  tasks: { id: string; title: string; done: boolean }[];
}
 
const addTask = action({
  name: "add-task",
  description: "Adds a new task to the todo list",
  schema: z.object({
    title: z.string().describe("What the task is"),
  }),
  handler: async ({ title }, ctx) => {
    // Access context memory (automatically typed!)
    const memory = ctx.memory as TodoMemory;
 
    // Initialize if needed
    if (!memory.tasks) {
      memory.tasks = [];
    }
 
    // Add new task
    const newTask = {
      id: crypto.randomUUID(),
      title,
      done: false,
    };
 
    memory.tasks.push(newTask);
 
    // Changes are automatically saved
    return {
      success: true,
      taskId: newTask.id,
      message: `Added "${title}" to your todo list`,
    };
  },
});
 
const completeTask = action({
  name: "complete-task",
  description: "Marks a task as completed",
  schema: z.object({
    taskId: z.string().describe("ID of task to complete"),
  }),
  handler: async ({ taskId }, ctx) => {
    const memory = ctx.memory as TodoMemory;
 
    const task = memory.tasks?.find((t) => t.id === taskId);
    if (!task) {
      return { success: false, message: "Task not found" };
    }
 
    task.done = true;
 
    return {
      success: true,
      message: `Completed "${task.title}"`,
    };
  },
});

Now your agent can manage todo lists across conversations!

External API Integration

Actions are perfect for calling external APIs:

external-api-action.ts
const searchWikipedia = action({
  name: "search-wikipedia",
  description: "Searches Wikipedia for information",
  schema: z.object({
    query: z.string().describe("What to search for"),
    limit: z.number().optional().default(3).describe("Max results"),
  }),
  handler: async ({ query, limit }) => {
    try {
      const response = await fetch(
        `https://en.wikipedia.org/api/rest_v1/page/search?q=${encodeURIComponent(
          query
        )}&limit=${limit}`
      );
 
      if (!response.ok) {
        throw new Error(`API error: ${response.status}`);
      }
 
      const data = await response.json();
 
      return {
        success: true,
        results: data.pages.map((page) => ({
          title: page.title,
          description: page.description,
          url: `https://en.wikipedia.org/wiki/${page.key}`,
        })),
        message: `Found ${data.pages.length} results for "${query}"`,
      };
    } catch (error) {
      return {
        success: false,
        error: error.message,
        message: `Failed to search Wikipedia for "${query}"`,
      };
    }
  },
});

Best Practices

1. Use Clear Names and Descriptions

good-naming.ts
// ✅ Good - clear what it does
const getUserProfile = action({
  name: "get-user-profile",
  description: "Gets detailed profile information for a user by their ID",
  // ...
});
 
// ❌ Bad - unclear purpose
const doStuff = action({
  name: "do-stuff",
  description: "Does some user stuff",
  // ...
});

2. Validate Input with Schemas

good-validation.ts
// ✅ Good - specific validation
schema: z.object({
  email: z.string().email().describe("User's email address"),
  age: z.number().min(0).max(150).describe("User's age in years"),
  preferences: z
    .array(z.string())
    .optional()
    .describe("List of user preferences"),
});
 
// ❌ Bad - too loose
schema: z.object({
  data: z.any(),
});

3. Handle Errors Gracefully

error-handling.ts
handler: async ({ userId }) => {
  try {
    const user = await database.getUser(userId);
    return { success: true, user };
  } catch (error) {
    // Log the error for debugging
    console.error("Failed to get user:", error);
 
    // Return structured error for the LLM
    return {
      success: false,
      error: "User not found",
      message: `Could not find user with ID ${userId}`,
    };
  }
};

4. Use async/await for I/O Operations

async-best-practice.ts
// ✅ Good - properly handles async
handler: async ({ url }) => {
  const response = await fetch(url);
  const data = await response.json();
  return { data };
};
 
// ❌ Bad - doesn't wait for async operations
handler: ({ url }) => {
  fetch(url); // This returns a Promise that's ignored!
  return { status: "done" }; // Completes before fetch finishes
};

5. Check for Cancellation in Long Operations

cancellation-handling.ts
handler: async ({ items }, ctx) => {
  for (let i = 0; i < items.length; i++) {
    // Check if the agent wants to cancel
    if (ctx.abortSignal?.aborted) {
      throw new Error("Operation cancelled");
    }
 
    await processItem(items[i]);
  }
 
  return { processed: items.length };
};

Advanced: Context-Specific Actions

You can attach actions to specific contexts so they're only available in certain situations:

context-specific.ts
const chatContext = context({
  type: "chat",
  schema: z.object({ userId: z.string() }),
  create: () => ({ messages: [] }),
}).setActions([
  // These actions only available during chat
  action({
    name: "save-chat-preference",
    description: "Saves a preference for this chat user",
    schema: z.object({
      key: z.string(),
      value: z.string(),
    }),
    handler: async ({ key, value }, ctx) => {
      // ctx.memory is automatically typed as chat memory
      if (!ctx.memory.userPreferences) {
        ctx.memory.userPreferences = {};
      }
      ctx.memory.userPreferences[key] = value;
      return { saved: true };
    },
  }),
]);

Key Takeaways

  • Actions give agents capabilities - They can do things, not just talk
  • LLM chooses when to use them - Based on names and descriptions you provide
  • Arguments are validated - Zod schemas ensure type safety
  • State persists automatically - Changes to memory are saved
  • Error handling is crucial - Return structured success/error responses
  • async/await required - For any I/O operations like API calls

Actions transform your agent from a chatbot into a capable assistant that can actually get things done.