Skip to content

Conversation

@github-actions
Copy link
Contributor

@github-actions github-actions bot commented Jan 27, 2026

This PR was opened by the Changesets release GitHub action. When you're ready to do a release, you can merge this and the packages will be published to npm automatically. If you're not ready to do a release yet, that's fine, whenever you add more changesets to main, this PR will be updated.

Releases

[email protected]

Patch Changes

  • #825 0c3c9bb Thanks @threepointone! - Add cursor-based pagination to getWorkflows(). Returns a WorkflowPage with workflows, total count, and cursor for next page. Default limit is 50 (max 100).

  • #825 0c3c9bb Thanks @threepointone! - Add workflow control methods: terminateWorkflow(), pauseWorkflow(), resumeWorkflow(), and restartWorkflow().

  • #799 d1a0c2b Thanks @threepointone! - feat: Add Cloudflare Workflows integration for Agents

    Adds seamless integration between Cloudflare Agents and Cloudflare Workflows for durable, multi-step background processing.

    Why use Workflows with Agents?

    Agents excel at real-time communication and state management, while Workflows excel at durable execution. Together:

    • Agents handle WebSocket connections and quick operations
    • Workflows handle long-running tasks, retries, and human-in-the-loop flows

    AgentWorkflow Base Class

    Extend AgentWorkflow instead of WorkflowEntrypoint to get typed access to the originating Agent:

    export class ProcessingWorkflow extends AgentWorkflow<MyAgent, TaskParams> {
      async run(event: AgentWorkflowEvent<TaskParams>, step: AgentWorkflowStep) {
        const params = event.payload;
    
        // Call Agent methods via RPC
        await this.agent.updateStatus(params.taskId, "processing");
    
        // Non-durable: progress reporting (lightweight, for frequent updates)
        await this.reportProgress({
          step: "process",
          percent: 0.5,
          message: "Halfway done"
        });
        this.broadcastToClients({ type: "update", taskId: params.taskId });
    
        // Durable via step: idempotent, won't repeat on retry
        await step.mergeAgentState({ taskProgress: 0.5 });
        await step.reportComplete(result);
    
        return result;
      }
    }

    Agent Methods

    • runWorkflow(workflowName, params, options?) - Start workflow with optional metadata for querying
    • sendWorkflowEvent(workflowName, workflowId, event) - Send events to waiting workflows
    • getWorkflow(workflowId) - Get tracked workflow by ID
    • getWorkflows(criteria?) - Query by status, workflowName, or metadata with pagination
    • deleteWorkflow(workflowId) - Delete a workflow tracking record
    • deleteWorkflows(criteria?) - Delete workflows by criteria (status, workflowName, metadata, createdBefore)
    • approveWorkflow(workflowId, data?) - Approve a waiting workflow
    • rejectWorkflow(workflowId, data?) - Reject a waiting workflow

    AgentWorkflow Methods

    On this (non-durable, lightweight):

    • reportProgress(progress) - Report typed progress object to Agent
    • broadcastToClients(message) - Broadcast to WebSocket clients
    • waitForApproval(step, opts?) - Wait for approval (throws on rejection)

    On step (durable, idempotent):

    • step.reportComplete(result?) - Report successful completion
    • step.reportError(error) - Report an error
    • step.sendEvent(event) - Send custom event to Agent
    • step.updateAgentState(state) - Replace Agent state (broadcasts to clients)
    • step.mergeAgentState(partial) - Merge into Agent state (broadcasts to clients)
    • step.resetAgentState() - Reset Agent state to initialState (broadcasts to clients)

    Lifecycle Callbacks

    Override these methods to handle workflow events (workflowName is first for easy differentiation):

    async onWorkflowProgress(workflowName, workflowId, progress) {} // progress is typed object
    async onWorkflowComplete(workflowName, workflowId, result?) {}
    async onWorkflowError(workflowName, workflowId, error) {}
    async onWorkflowEvent(workflowName, workflowId, event) {}

    Workflow Tracking

    Workflows are automatically tracked in cf_agents_workflows SQLite table:

    • Status, timestamps, errors
    • Optional metadata field for queryable key-value data
    • Params/output NOT stored by default (could be large)

    See docs/workflows.md for full documentation.

  • #812 6218541 Thanks @threepointone! - # Bug Fixes

    This release includes three bug fixes:

    1. Hung Callback Detection in scheduleEvery()

    Fixed a deadlock where if an interval callback hung indefinitely, all future interval executions would be skipped forever.

    Fix: Track execution start time and force reset after 30 seconds of inactivity. If a previous execution appears hung (started more than 30s ago), it is force-reset and re-executed.

    // Now safe - hung callbacks won't block future executions
    await this.scheduleEvery(60, "myCallback");

    2. Corrupted State Recovery

    Fixed a crash when the database contains malformed JSON state.

    Fix: Wrapped JSON.parse in try-catch with fallback to initialState. If parsing fails, the agent logs an error and recovers gracefully.

    // Agent now survives corrupted state
    class MyAgent extends Agent {
      initialState = { count: 0 }; // Used as fallback if DB state is corrupted
    }

    3. getCallableMethods() Prototype Chain Traversal

    Fixed getCallableMethods() to find @callable methods from parent classes, not just the immediate class.

    Fix: Walk the full prototype chain using Object.getPrototypeOf() loop.

    class BaseAgent extends Agent {
      @callable()
      parentMethod() {
        return "parent";
      }
    }
    
    class ChildAgent extends BaseAgent {
      @callable()
      childMethod() {
        return "child";
      }
    }
    
    // Now correctly returns both parentMethod and childMethod
    const methods = childAgent.getCallableMethods();
  • #812 6218541 Thanks @threepointone! - # Callable System Improvements

    This release includes several improvements to the @callable decorator and RPC system:

    New Features

    Client-side RPC Timeout

    You can now specify a timeout for RPC calls that will reject if the call doesn't complete in time:

    await agent.call("slowMethod", [], { timeout: 5000 });

    StreamingResponse.error()

    New method to gracefully signal an error during streaming and close the stream:

    @callable({ streaming: true })
    async processItems(stream: StreamingResponse, items: string[]) {
      for (const item of items) {
        try {
          const result = await this.process(item);
          stream.send(result);
        } catch (e) {
          stream.error(`Failed to process ${item}: ${e.message}`);
          return;
        }
      }
      stream.end();
    }

    getCallableMethods() API

    New method on the Agent class to introspect all callable methods and their metadata:

    const methods = agent.getCallableMethods();
    // Returns Map<string, CallableMetadata>
    
    for (const [name, meta] of methods) {
      console.log(`${name}: ${meta.description || "(no description)"}`);
    }

    Connection Close Handling

    Pending RPC calls are now automatically rejected with a "Connection closed" error when the WebSocket connection closes unexpectedly.

    Internal Improvements

    • WeakMap for metadata storage: Changed callableMetadata from Map to WeakMap to prevent memory leaks when function references are garbage collected.
    • UUID for RPC IDs: Replaced Math.random().toString(36) with crypto.randomUUID() for more robust and unique RPC call identifiers.
    • Streaming observability: Added observability events for streaming RPC calls.

    API Enhancements

    The agent.call() method now accepts a unified CallOptions object with timeout support:

    // New format (preferred, supports timeout)
    await agent.call("method", [args], {
      timeout: 5000,
      stream: { onChunk, onDone, onError }
    });
    
    // Legacy format (still fully supported for backward compatibility)
    await agent.call("method", [args], { onChunk, onDone, onError });

    Both formats work seamlessly - the client auto-detects which format you're using.

  • #812 6218541 Thanks @threepointone! - feat: Add scheduleEvery method for fixed-interval scheduling

    Adds a new scheduleEvery(intervalSeconds, callback, payload?) method to the Agent class for scheduling recurring tasks at fixed intervals.

    Features

    • Fixed interval execution: Schedule a callback to run every N seconds
    • Overlap prevention: If a callback is still running when the next interval fires, the next execution is skipped
    • Error resilience: If a callback throws, the schedule persists and continues on the next interval
    • Cancellable: Use cancelSchedule(id) to stop the recurring schedule

    Usage

    class MyAgent extends Agent {
      async onStart() {
        // Run cleanup every 60 seconds
        await this.scheduleEvery(60, "cleanup");
    
        // With payload
        await this.scheduleEvery(300, "syncData", { source: "api" });
      }
    
      cleanup() {
        // Runs every 60 seconds
      }
    
      syncData(payload: { source: string }) {
        // Runs every 300 seconds with payload
      }
    }

    Querying interval schedules

    // Get all interval schedules
    const intervals = await this.getSchedules({ type: "interval" });

    Schema changes

    Adds intervalSeconds and running columns to cf_agents_schedules table (auto-migrated for existing agents).

  • #812 6218541 Thanks @threepointone! - Add isAutoReplyEmail() utility to detect auto-reply emails

    Detects auto-reply emails based on standard RFC 3834 headers (Auto-Submitted, X-Auto-Response-Suppress, Precedence). Use this to avoid mail loops when sending automated replies.

    import { isAutoReplyEmail } from "agents/email";
    import PostalMime from "postal-mime";
    
    async onEmail(email: AgentEmail) {
      const raw = await email.getRaw();
      const parsed = await PostalMime.parse(raw);
    
      // Detect and skip auto-reply emails
      if (isAutoReplyEmail(parsed.headers)) {
        console.log("Skipping auto-reply");
        return;
      }
    
      // Process the email...
    }
  • #781 fd79481 Thanks @HueCodes! - fix: properly type tool error content in getAITools

  • #812 6218541 Thanks @threepointone! - fix: improve type inference for RPC methods returning custom interfaces

    Previously, RPCMethod used { [key: string]: SerializableValue } to check if return types were serializable. This didn't work with TypeScript interfaces that have named properties (like interface CoreState { counter: number; name: string; }), causing those methods to be incorrectly excluded from typed RPC calls.

    Now uses a recursive CanSerialize<T> type that checks if all properties of an object are serializable, properly supporting:

    • Custom interfaces with named properties
    • Nested object types
    • Arrays of objects
    • Optional and nullable properties
    • Union types

    Also expanded NonSerializable to explicitly exclude non-JSON-serializable types like Date, RegExp, Map, Set, Error, and typed arrays.

    // Before: these methods were NOT recognized as callable
    interface MyState {
      counter: number;
      items: string[];
    }
    
    class MyAgent extends Agent<Env, MyState> {
      @callable()
      getState(): MyState {
        return this.state;
      } // ❌ Not typed
    }
    
    // After: properly recognized and typed
    const agent = useAgent<MyAgent, MyState>({ agent: "my-agent" });
    agent.call("getState"); // ✅ Typed as Promise<MyState>
  • #825 0c3c9bb Thanks @threepointone! - Fix workflow tracking table not being updated by AgentWorkflow callbacks.

    Previously, when a workflow reported progress, completion, or errors via callbacks, the cf_agents_workflows tracking table was not updated. This caused getWorkflow() and getWorkflows() to return stale status (e.g., "queued" instead of "running" or "complete").

    Now, onWorkflowCallback() automatically updates the tracking table:

    • Progress callbacks set status to "running"
    • Complete callbacks set status to "complete" with completed_at timestamp
    • Error callbacks set status to "errored" with error details

    Fixes Workflow Tracking Table Not Updated by AgentWorkflow Callbacks #821.

  • #812 6218541 Thanks @threepointone! - feat: Add options-based API for addMcpServer

    Adds a cleaner options-based overload for addMcpServer() that avoids passing undefined for unused positional parameters.

    Before (still works)

    // Awkward when you only need transport options
    await this.addMcpServer("server", url, undefined, undefined, {
      transport: { headers: { Authorization: "Bearer ..." } }
    });

    After (preferred)

    // Clean options object
    await this.addMcpServer("server", url, {
      transport: { headers: { Authorization: "Bearer ..." } }
    });
    
    // With callback host
    await this.addMcpServer("server", url, {
      callbackHost: "https://my-worker.workers.dev",
      transport: { type: "sse" }
    });

    Options

    type AddMcpServerOptions = {
      callbackHost?: string; // OAuth callback host (auto-derived if omitted)
      agentsPrefix?: string; // Routing prefix (default: "agents")
      client?: ClientOptions; // MCP client options
      transport?: {
        headers?: HeadersInit; // Custom headers for auth
        type?: "sse" | "streamable-http" | "auto";
      };
    };

    The legacy 5-parameter signature remains fully supported for backward compatibility.

  • #812 6218541 Thanks @threepointone! - Add custom URL routing with basePath and server-sent identity

    Custom URL Routing with basePath

    New basePath option bypasses default /agents/{agent}/{name} URL construction, enabling custom routing patterns:

    // Client connects to /user instead of /agents/user-agent/...
    const agent = useAgent({
      agent: "UserAgent",
      basePath: "user"
    });

    Server handles routing manually with getAgentByName:

    export default {
      async fetch(request: Request, env: Env) {
        const url = new URL(request.url);
    
        if (url.pathname === "/user") {
          const session = await getSession(request);
          const agent = await getAgentByName(env.UserAgent, session.userId);
          return agent.fetch(request);
        }
    
        return (
          (await routeAgentRequest(request, env)) ??
          new Response("Not found", { status: 404 })
        );
      }
    };

    Server-Sent Identity

    Agents now send their identity (name and agent class) to clients on connect:

    • onIdentity callback - called when server sends identity
    • agent.name and agent.agent are updated from server (authoritative)
    const agent = useAgent({
      agent: "UserAgent",
      basePath: "user",
      onIdentity: (name, agentType) => {
        console.log(`Connected to ${agentType} instance: ${name}`);
      }
    });

    Identity State & Ready Promise

    • identified: boolean - whether identity has been received
    • ready: Promise<void> - resolves when identity is received
    • In React, name, agent, and identified are reactive state
    // React - reactive rendering
    return agent.identified ? `Connected to: ${agent.name}` : "Connecting...";
    
    // Vanilla JS - await ready
    await agent.ready;
    console.log(agent.name);

    Identity Change Detection

    • onIdentityChange callback - fires when identity differs on reconnect
    • Warns if identity changes without handler (helps catch session issues)
    useAgent({
      basePath: "user",
      onIdentityChange: (oldName, newName, oldAgent, newAgent) => {
        console.log(`Session changed: ${oldName}${newName}`);
      }
    });

    Sub-Paths with path Option

    Append additional path segments:

    // /user/settings
    useAgent({ basePath: "user", path: "settings" });
    
    // /agents/my-agent/room/settings
    useAgent({ agent: "MyAgent", name: "room", path: "settings" });

    Server-Side Identity Control

    Disable identity sending for security-sensitive instance names:

    class SecureAgent extends Agent {
      static options = { sendIdentityOnConnect: false };
    }
  • #827 e20da53 Thanks @threepointone! - Move workflow exports to agents/workflows subpath for better separation of concerns.

    import { AgentWorkflow } from "agents/workflows";
    import type { AgentWorkflowStep, WorkflowInfo } from "agents/workflows";
  • #811 f604008 Thanks @threepointone! - ### Secure Email Reply Routing

    This release introduces secure email reply routing with HMAC-SHA256 signed headers, preventing unauthorized routing of emails to arbitrary agent instances.

    Breaking Changes

    Email utilities moved to agents/email subpath: Email-specific resolvers and utilities have been moved to a dedicated subpath for better organization.

    // Before
    import { createAddressBasedEmailResolver, signAgentHeaders } from "agents";
    
    // After
    import {
      createAddressBasedEmailResolver,
      signAgentHeaders
    } from "agents/email";

    The following remain in root: routeAgentEmail, createHeaderBasedEmailResolver (deprecated).

    createHeaderBasedEmailResolver removed: This function now throws an error with migration guidance. It was removed because it trusted attacker-controlled email headers for routing.

    Migration:

    • For inbound mail: use createAddressBasedEmailResolver(agentName)
    • For reply flows: use createSecureReplyEmailResolver(secret) with signed headers

    See https://github.com/cloudflare/agents/blob/main/docs/email.md for details.

    EmailSendOptions type removed: This type was unused and has been removed.

    New Features

    createSecureReplyEmailResolver: A new resolver that verifies HMAC-SHA256 signatures on incoming emails before routing. Signatures include a timestamp and expire after 30 days by default.

    const resolver = createSecureReplyEmailResolver(env.EMAIL_SECRET, {
      maxAge: 7 * 24 * 60 * 60, // Optional: 7 days (default: 30 days)
      onInvalidSignature: (email, reason) => {
        // Optional: log failures for debugging
        // reason: "missing_headers" | "expired" | "invalid" | "malformed_timestamp"
        console.warn(`Invalid signature from ${email.from}: ${reason}`);
      }
    });

    signAgentHeaders: Helper function to manually sign agent routing headers for use with external email services.

    const headers = await signAgentHeaders(secret, agentName, agentId);
    // Returns: { "X-Agent-Name", "X-Agent-ID", "X-Agent-Sig", "X-Agent-Sig-Ts" }

    replyToEmail signing: The replyToEmail method now accepts a secret option to automatically sign outbound email headers.

    await this.replyToEmail(email, {
      fromName: "My Agent",
      body: "Thanks!",
      secret: this.env.EMAIL_SECRET // Signs headers for secure reply routing
    });

    If an email was routed via createSecureReplyEmailResolver, calling replyToEmail without a secret will throw an error (pass explicit null to opt-out).

    onNoRoute callback: routeAgentEmail now accepts an onNoRoute callback for handling emails that don't match any routing rule.

    await routeAgentEmail(message, env, {
      resolver,
      onNoRoute: (email) => {
        email.setReject("Unknown recipient");
      }
    });
  • #813 7aebab3 Thanks @threepointone! - update dependencies

  • #800 a54edf5 Thanks @threepointone! - Update dependencies

  • #818 7c74336 Thanks @threepointone! - update dependencies

  • #812 6218541 Thanks @threepointone! - # Synchronous setState with validation hook

    setState() is now synchronous instead of async. This improves ergonomics and aligns with the expected mental model for state updates.

    Breaking Changes

    setState() returns void instead of Promise<void>

    // Before (still works - awaiting a non-promise is harmless)
    await this.setState({ count: 1 });
    
    // After (preferred)
    this.setState({ count: 1 });

    Existing code that uses await this.setState(...) will continue to work without changes.

    onStateUpdate() no longer gates state broadcasts

    Previously, if onStateUpdate() threw an error, the state update would be aborted. Now, onStateUpdate() runs asynchronously via ctx.waitUntil() after the state is persisted and broadcast. Errors in onStateUpdate() are routed to onError() but do not prevent the state from being saved or broadcast.

    If you were using onStateUpdate() for validation, migrate to validateStateChange().

    New Features

    validateStateChange() validation hook

    A new synchronous hook that runs before state is persisted or broadcast. Use this for validation:

    validateStateChange(nextState: State, source: Connection | "server") {
      if (nextState.count < 0) {
        throw new Error("Count cannot be negative");
      }
    }
    • Runs synchronously before persistence and broadcast
    • Throwing aborts the state update entirely
    • Ideal for validation logic

    Execution order

    1. validateStateChange(nextState, source) - validation (sync, gating)
    2. State persisted to SQLite
    3. State broadcast to connected clients
    4. onStateUpdate(nextState, source) - notifications (async via ctx.waitUntil, non-gating)
  • #815 ded8d3e Thanks @threepointone! - docs: add OpenAI provider options documentation to scheduleSchema

    When using scheduleSchema with OpenAI models via the AI SDK, users must now pass providerOptions: { openai: { strictJsonSchema: false } } to generateObject. This is documented in the JSDoc for scheduleSchema.

    This is required because @ai-sdk/openai now defaults strictJsonSchema to true, which requires all schema properties to be in the required array. The scheduleSchema uses optional fields which are not compatible with this strict mode.

  • Updated dependencies [7aebab3, 77be4f8, a54edf5, 7c74336, 99cbca0]:

@cloudflare/[email protected]

Patch Changes

@cloudflare/[email protected]

Patch Changes

[email protected]

Patch Changes

@github-actions github-actions bot force-pushed the changeset-release/main branch 12 times, most recently from 356b24d to 2bfea92 Compare January 30, 2026 11:35
Copy link
Contributor

@threepointone threepointone left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

blocking release

@github-actions github-actions bot force-pushed the changeset-release/main branch 6 times, most recently from 04162f9 to aa361a1 Compare February 2, 2026 13:02
@github-actions github-actions bot force-pushed the changeset-release/main branch from aa361a1 to 1902be0 Compare February 3, 2026 11:01
This release introduces Cloudflare Workflows integration, secure email reply routing with HMAC-SHA256 signatures, 15+ new documentation files, and significant improvements to state management, callable RPC system, and scheduling.
@claude
Copy link

claude bot commented Feb 3, 2026

Claude Code Review

This is a standard Changesets release PR for version 0.3.7. All changes look correct:

✅ Version bumps are consistent (agents: 0.3.7, ai-chat: 0.0.5, codemode: 0.0.6, hono-agents: 3.0.2)
✅ Changeset files properly consumed and deleted
✅ CHANGELOGs correctly updated with release notes
✅ package-lock.json updated accordingly

No issues found. Ready to merge and publish.

@pkg-pr-new
Copy link

pkg-pr-new bot commented Feb 3, 2026

Open in StackBlitz

npm i https://pkg.pr.new/cloudflare/agents@802

commit: b477cf3

Copy link
Contributor

@threepointone threepointone left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

good to go

@threepointone threepointone merged commit 9647577 into main Feb 3, 2026
6 checks passed
@threepointone threepointone deleted the changeset-release/main branch February 3, 2026 11:12
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Workflow Tracking Table Not Updated by AgentWorkflow Callbacks

1 participant