Skip to main content
GET
JavaScript
An objective runs in the background. This endpoint is how you watch it: a server-sent event stream that pushes each event the moment it lands, so you can render a live transcript instead of polling. The endpoint takes no query parameters. Connect, and you get events from now forward.

Read the stream

The SDKs hand you an iterator and skip the transport bookkeeping.
In TypeScript, ObjectiveEvent.data is a discriminated union. Each case narrows data to the matching event interface, so the corresponding payload is available without a cast.
The stream never closes on its own. Reaching STATE_FINALIZED does not end the connection. It stays open, emitting a ping every 15 seconds, until the load balancer severs it at 10 minutes.Break on the terminal event type, never on end-of-stream. A loop with no break holds a connection for ten minutes after the objective finishes, and a client that reconnects whenever the stream ends reconnects forever on a completed objective.

What arrives on the wire

Each event frame carries three SSE fields: id is the event’s ULID, event is the event type (assistantMessage, toolCalled, …), and data is the JSON ObjectiveEvent. Two control frames carry no id and are not objective events:
  • open, once, when the stream commits. Its arrival means the connection is live and the status code is settled.
  • ping, every 15 seconds, to keep intermediaries from closing an idle connection.
The SDKs drop open and ping before they reach your loop. If you read the raw stream, skip them yourself: they do not parse as an ObjectiveEvent.

Tool results are stripped

This is the one surprise worth internalizing. On the stream, toolResult and toolError events carry only the toolCallId. The content is cleared, because a tool that returns a megabyte of JSON would otherwise push it through every open connection.
Streamed
Same event from listEvents
Every other event type passes through whole. When you need a result body, fetch the call by ID with Get a tool call, which returns result at the top level, or read the event back from List objective events.

Built-in tool calls arrive unnamed

toolCalled.tool is a discriminated union: its type field reads tool for a tool from a tool set, agent for a sub-agent, or cadenyaProvidedTool for a built-in such as get_memory or tool_search, and the payload sits under the matching key. Switch on type, never reach through tool.tool. The built-in case carries an opaque cpt_... id and an empty name. arguments tells you what it did ({"memoryKey": "policies/us/refunds"}), and Get a tool call returns the same call with callable.cadenyaProvidedTool.name filled in.

Resume where you left off

Send the SSE Last-Event-ID header with the last event ULID you processed. Cadenya replays the gap from durable storage, then switches you to the live feed, deduplicating the overlap. Omit the header and you get events from now, with no replay of anything the objective already emitted.
Neither SDK reconnects for you, and neither tracks the last event ID. The loop above is the pattern to copy. A single connection lives at most 10 minutes by design, so any objective that runs longer requires this reconnect loop.
An unusable Last-Event-ID fails before the stream commits, so it comes back as a real HTTP status rather than a mid-stream error. Once open arrives the status is committed, and any later failure ends the connection with no status to read.

Every event type

Eighteen types. data is a discriminated union: type names the variant, and the payload sits under a key with the same name. The four terminal types are the ones your loop must watch for.
An objective that answers and waits for your next turn never emits a terminal event. It sits in STATE_WAITING, and the stream stays open. Break on the last assistantMessage when you are driving a conversation rather than waiting on an output.

Streaming or webhooks

Both carry the same events. They fail differently, and that is how you choose. Stream when a human is watching: a chat UI, a live transcript, a progress log. It is a live tail with no delivery guarantee beyond the Last-Event-ID resume path, and it needs a process holding a connection open. Webhook when a machine is reacting: kicking off a job, writing to a database, paging someone. Deliveries retry, and each one records its status, attempt count, and response, so you can audit what your endpoint received. Reach for both on the same objective when a person watches the work while a system records it.

Authorizations

Authorization
string
header
required

Bearer authentication header of the form Bearer <token>, where <token> is your auth token.

Path Parameters

workspaceId
string
required
Example:

"workspace_01HXKD2E5NQM3T9AYWCF133E3Q"

objectiveId
string
required
Example:

"obj_01HXKD2E5NQM3T9AYWCFQAZGFV"

Response

A stream of Server-Sent Events

metadata
object
required

Metadata for ephemeral operations and activities (e.g., objectives, executions, runs)

data
object
required
contextWindowId
string
Example:

"objwin_01HXKD2E5NQM3T9AYWCFN7BSTR"

info
object
duration
string
read-only

Elapsed time of the work this event records, when it is known at write time (e.g. assistant message generation, tool execution for result/error events). Unset means the event is instantaneous or the duration is not measurable. Serialized as a canonical duration string (e.g. "4.1s"). Always set together with started_at.

Pattern: ^-?(?:0|[1-9][0-9]{0,11})(?:\.[0-9]{1,9})?s$
startedAt
string<date-time>
read-only

When the work this event records began. Set together with duration, so the work interval is [started_at, started_at + duration]. The event's created_at remains the time the event was persisted.