Daydreams Logo
Concepts

Outputs

How Daydreams agents send information and responses.

What is an Output?

An output is how your agent sends information to the outside world. If actions are what your agent can "do", outputs are how your agent "speaks" or "responds".

Real Examples

Here are outputs that make agents useful:

Discord Message

discord-output.ts
// Agent can send Discord messages
const discordMessage = output({
  type: "discord:message",
  description: "Sends a message to Discord",
  schema: z.string(),
  attributes: z.object({
    channelId: z.string(),
  }),
  handler: async (message, ctx) => {
    await discord.send(ctx.outputRef.params.channelId, message);
    return { sent: true };
  },
});

Console Print

console-output.ts
// Agent can print to console
const consolePrint = output({
  type: "console:print",
  description: "Prints a message to the console",
  schema: z.string(),
  handler: async (message) => {
    console.log(`Agent: ${message}`);
    return { printed: true };
  },
});

Email Notification

email-output.ts
// Agent can send emails
const emailOutput = output({
  type: "email:send",
  description: "Sends an email notification",
  schema: z.string(),
  attributes: z.object({
    to: z.string(),
    subject: z.string(),
  }),
  handler: async (body, ctx) => {
    const { to, subject } = ctx.outputRef.params;
    await emailService.send({ to, subject, body });
    return { emailSent: true };
  },
});

The Problem: Agents Need to Communicate

Without outputs, your agent can think but can't communicate:

silent-agent.txt
User: "Send me the weather report"
Agent: *calls weather API internally*
Agent: *knows it's 72°F and sunny*
Agent: *...but can't tell you!*
// ❌ Agent gets the data but you never see it
// ❌ No way to respond or communicate
// ❌ Useless to humans

The Solution: Outputs Enable Communication

With outputs, your agent can respond properly:

communicating-agent.txt
User: "Send me the weather report"
Agent: *calls weather API*
Agent: *gets weather data*
Agent: *uses discord:message output*
Discord: "It's 72°F and sunny in San Francisco!"
// ✅ Agent gets data AND tells you about it
// ✅ Complete conversation loop
// ✅ Actually useful

How Outputs Work in Your Agent

1. You Define How the Agent Can Respond

define-outputs.ts
const agent = createDreams({
  model: openai("gpt-4o"),
  outputs: [
    discordMessage, // Agent can send Discord messages
    consolePrint, // Agent can print to console
    emailOutput, // Agent can send emails
  ],
});

2. The LLM Decides When to Respond

When the agent thinks, it sees:

llm-sees-outputs.txt
Available outputs:
- discord:message: Sends a message to Discord
- console:print: Prints a message to the console
- email:send: Sends an email notification

User asked: "Check weather and let me know via Discord"

3. The LLM Uses Outputs to Respond

The LLM responds with structured output calls:

llm-uses-outputs.xml
<response>
  <reasoning>User wants weather info via Discord. I'll get weather then send message.</reasoning>
 
  <action_call name="get-weather">{"city": "San Francisco"}</action_call>
 
  <output type="discord:message" channelId="123456789">
    Weather in San Francisco: {{calls[0].temperature}}, {{calls[0].condition}}
  </output>
</response>

4. Daydreams Sends the Output

Daydreams automatically:

  • Validates the output format
  • Runs your handler function
  • Actually sends the Discord message
  • Logs the result

Creating Your First Output

Here's a simple output that saves messages to a file:

file-output.ts
import { output } from "@daydreamsai/core";
import { z } from "zod";
import fs from "fs/promises";
 
export const saveToFile = output({
  // Type the LLM uses to call this output
  type: "file:save",
 
  // Description helps LLM know when to use it
  description: "Saves a message to a text file",
 
  // Schema defines what content is expected
  schema: z.string().describe("The message to save"),
 
  // Attributes define extra parameters on the output tag
  attributes: z.object({
    filename: z.string().describe("Name of the file to save to"),
  }),
 
  // Handler is your actual code that runs
  handler: async (message, ctx) => {
    const { filename } = ctx.outputRef.params;
 
    await fs.writeFile(filename, message + "\n", { flag: "a" });
 
    return {
      saved: true,
      filename,
      message: `Saved message to ${filename}`,
    };
  },
});

Use it in your agent:

agent-with-file-output.ts
const agent = createDreams({
  model: openai("gpt-4o"),
  outputs: [saveToFile],
});
 
// Now when the LLM wants to save something:
// <output type="file:save" filename="log.txt">This is my message</output>
// The message gets saved to log.txt

Working with Context Memory

Outputs can read and update your agent's memory:

notification-output.ts
// Define what your context remembers
interface ChatMemory {
  messagesSent: number;
  lastNotification?: string;
}
 
const notificationOutput = output({
  type: "notification:send",
  description: "Sends a notification to the user",
  schema: z.string(),
  attributes: z.object({
    priority: z.enum(["low", "medium", "high"]),
  }),
  handler: async (message, ctx) => {
    // Access context memory (automatically typed!)
    const memory = ctx.memory as ChatMemory;
 
    // Update statistics
    if (!memory.messagesSent) {
      memory.messagesSent = 0;
    }
    memory.messagesSent++;
    memory.lastNotification = message;
 
    // Send the actual notification
    const { priority } = ctx.outputRef.params;
    await notificationService.send({
      message,
      priority,
      userId: ctx.args.userId,
    });
 
    // Changes to memory are automatically saved
    return {
      sent: true,
      totalSent: memory.messagesSent,
      message: `Notification sent (total: ${memory.messagesSent})`,
    };
  },
});

Outputs vs Actions: When to Use Which?

Understanding the difference is crucial:

Use Outputs When:

  • Communicating results to users or external systems
  • You don't need a response back for the LLM to continue
  • Final step in a conversation or workflow
output-example.ts
// ✅ Good use of output - telling user the result
<output type="discord:message" channelId="123">
  Weather: 72°F, sunny. Have a great day!
</output>

Use Actions When:

  • Getting data the LLM needs for next steps
  • You need the result for further reasoning
  • Middle step in a complex workflow
action-example.ts
// ✅ Good use of action - getting data for next step
<action_call name="get-weather">{"city": "San Francisco"}</action_call>
// LLM will use this result to decide what to tell the user

Common Pattern: Actions → Outputs

action-then-output.xml
<response>
  <reasoning>I'll get weather data, then tell the user about it</reasoning>
 
  <!-- Action: Get data -->
  <action_call name="get-weather">{"city": "Boston"}</action_call>
 
  <!-- Output: Communicate result -->
  <output type="discord:message" channelId="123">
    Boston weather: {{calls[0].temperature}}, {{calls[0].condition}}
  </output>
</response>

Advanced: Multiple Outputs

Your agent can send multiple outputs in one response:

multiple-outputs.xml
<response>
  <reasoning>I'll notify both Discord and email about this important update</reasoning>
 
  <output type="discord:message" channelId="123">
    🚨 Server maintenance starting in 10 minutes!
  </output>
 
  <output type="email:send" to="admin@company.com" subject="Maintenance Alert">
    Server maintenance is beginning in 10 minutes. All users have been notified via Discord.
  </output>
</response>

External Service Integration

Outputs are perfect for integrating with external services:

slack-output.ts
const slackMessage = output({
  type: "slack:message",
  description: "Sends a message to Slack",
  schema: z.string(),
  attributes: z.object({
    channel: z.string().describe("Slack channel name"),
    threadId: z.string().optional().describe("Thread ID for replies"),
  }),
  handler: async (message, ctx) => {
    try {
      const { channel, threadId } = ctx.outputRef.params;
 
      const result = await slackClient.chat.postMessage({
        channel,
        text: message,
        thread_ts: threadId,
      });
 
      return {
        success: true,
        messageId: result.ts,
        channel: result.channel,
        message: `Message sent to #${channel}`,
      };
    } catch (error) {
      console.error("Failed to send Slack message:", error);
 
      return {
        success: false,
        error: error.message,
        message: "Failed to send Slack message",
      };
    }
  },
});

Best Practices

1. Use Clear Types and Descriptions

good-naming.ts
// ✅ Good - clear what it does
const userNotification = output({
  type: "user:notification",
  description:
    "Sends a notification directly to the user via their preferred channel",
  // ...
});
 
// ❌ Bad - unclear purpose
const sendStuff = output({
  type: "send",
  description: "Sends something",
  // ...
});

2. Validate Input with Schemas

good-schemas.ts
// ✅ Good - specific validation
schema: z.object({
  title: z.string().min(1).max(100),
  content: z.string().min(1).max(2000),
  urgency: z.enum(["low", "medium", "high"]),
});
 
// ❌ Bad - too loose
schema: z.any();

3. Handle Errors Gracefully

error-handling.ts
handler: async (message, ctx) => {
  try {
    await sendMessage(message);
    return { sent: true };
  } catch (error) {
    // Log for debugging
    console.error("Failed to send message:", error);
 
    // Return structured error info
    return {
      sent: false,
      error: error.message,
      message: "Failed to send message - will retry later",
    };
  }
};

4. Use Async/Await for External Services

async-best-practice.ts
// ✅ Good - properly handles async
handler: async (message, ctx) => {
  const result = await emailService.send(message);
  return { emailId: result.id };
};
 
// ❌ Bad - doesn't wait for async operations
handler: (message, ctx) => {
  emailService.send(message); // This returns a Promise that's ignored!
  return { status: "sent" }; // Completes before email actually sends
};

5. Provide Good Examples

good-examples.ts
examples: [
  '<output type="discord:message" channelId="123456789">Hello everyone!</output>',
  '<output type="discord:message" channelId="987654321" replyToUserId="user123">Thanks for the question!</output>',
];

Key Takeaways

  • Outputs enable communication - Without them, agents can think but not respond
  • LLM chooses when to use them - Based on types and descriptions you provide
  • Different from actions - Outputs communicate results, actions get data
  • Content and attributes validated - Zod schemas ensure correct format
  • Memory can be updated - Track what was sent for future reference
  • Error handling is crucial - External services can fail, handle gracefully

Outputs complete the conversation loop - they're how your intelligent agent becomes a helpful communicator that users can actually interact with.