diff --git a/apps/sim/app/workspace/[workspaceId]/logs/components/log-details/components/trace-view/trace-view.tsx b/apps/sim/app/workspace/[workspaceId]/logs/components/log-details/components/trace-view/trace-view.tsx
index 6b5f0a54988..cfc91ed20fa 100644
--- a/apps/sim/app/workspace/[workspaceId]/logs/components/log-details/components/trace-view/trace-view.tsx
+++ b/apps/sim/app/workspace/[workspaceId]/logs/components/log-details/components/trace-view/trace-view.tsx
@@ -724,7 +724,8 @@ const TraceDetailPane = memo(function TraceDetailPane({ span }: { span: TraceSpa
if (span.iterationIndex !== undefined)
metaEntries.push({ label: 'Iteration', value: String(span.iterationIndex + 1) })
- const statusLabel = hasError ? 'Error' : 'Success'
+ const statusLabel =
+ isDirectError && span.errorHandled ? 'Handled error' : hasError ? 'Error' : 'Success'
return (
diff --git a/apps/sim/app/workspace/[workspaceId]/logs/components/log-details/utils.ts b/apps/sim/app/workspace/[workspaceId]/logs/components/log-details/utils.ts
index 7b05e1dcf23..7c26e671dee 100644
--- a/apps/sim/app/workspace/[workspaceId]/logs/components/log-details/utils.ts
+++ b/apps/sim/app/workspace/[workspaceId]/logs/components/log-details/utils.ts
@@ -38,7 +38,9 @@ export function isIterationType(type: string): boolean {
export function hasErrorInTree(span: TraceSpan): boolean {
if (span.status === 'error') return true
- if (span.children?.length) return span.children.some(hasErrorInTree)
+ if (span.children?.length) {
+ return span.children.some((child) => hasUnhandledError(child, { includeToolCalls: true }))
+ }
if (span.toolCalls?.length) return span.toolCalls.some((tc) => tc.error)
return false
}
diff --git a/apps/sim/executor/types.ts b/apps/sim/executor/types.ts
index c51495ed3cd..f87b9df6cf9 100644
--- a/apps/sim/executor/types.ts
+++ b/apps/sim/executor/types.ts
@@ -191,6 +191,8 @@ export interface BlockTokens {
/** A single tool invocation recorded by an agent-type block. */
export interface BlockToolCall {
name: string
+ success?: boolean
+ status?: string
duration?: number
startTime?: string
endTime?: string
diff --git a/apps/sim/lib/logs/execution/trace-spans/span-factory.ts b/apps/sim/lib/logs/execution/trace-spans/span-factory.ts
index af9b981aed5..f2ac2f55cdd 100644
--- a/apps/sim/lib/logs/execution/trace-spans/span-factory.ts
+++ b/apps/sim/lib/logs/execution/trace-spans/span-factory.ts
@@ -1,5 +1,6 @@
import { createLogger } from '@sim/logger'
import { isRecordLike } from '@sim/utils/object'
+import { truncate } from '@sim/utils/string'
import type { ProviderTiming, TraceSpan } from '@/lib/logs/types'
import {
CHILD_EXECUTION_ID_OUTPUT_KEY,
@@ -16,6 +17,7 @@ import type {
} from '@/executor/types'
const logger = createLogger('SpanFactory')
+const TOOL_CALL_ERROR_MAX_LENGTH = 4096
/** A BlockLog that has already passed the id/type validity check. */
type ValidBlockLog = BlockLog & { blockType: string }
@@ -26,6 +28,49 @@ function normalizeTraceOutput(value: unknown): Record | undefin
return isRecordLike(value) ? value : { value }
}
+function nonEmptyString(value: unknown): string | undefined {
+ return typeof value === 'string' && value.trim().length > 0 ? value : undefined
+}
+
+/**
+ * Returns the canonical error message for a failed agent tool call.
+ *
+ * Providers expose failures through several normalized shapes. A nested
+ * `success: false` is explicit; the generic nested fallback requires Sim's
+ * complete error envelope so successful tool data with an `error` field is
+ * not misclassified.
+ */
+function getToolCallErrorMessage(
+ toolCall: BlockToolCall | undefined,
+ segmentErrorMessage?: string
+): string | undefined {
+ const rawResult = toolCall?.result ?? toolCall?.output
+ const result = isRecordLike(rawResult) ? rawResult : undefined
+ const topLevelError = nonEmptyString(toolCall?.error)
+ const segmentError = nonEmptyString(segmentErrorMessage)
+ const hasExplicitFailure =
+ toolCall?.success === false ||
+ toolCall?.status === 'error' ||
+ result?.success === false ||
+ topLevelError !== undefined ||
+ segmentError !== undefined
+ const hasStandardSimError =
+ result?.error === true &&
+ nonEmptyString(result.message) !== undefined &&
+ nonEmptyString(result.tool) !== undefined
+
+ if (!hasExplicitFailure && !hasStandardSimError) return undefined
+
+ const nestedMessage = nonEmptyString(result?.message) ?? nonEmptyString(result?.error)
+ const message =
+ topLevelError ??
+ segmentError ??
+ nestedMessage ??
+ `Tool ${toolCall?.name || 'call'} execution failed`
+
+ return truncate(message, TOOL_CALL_ERROR_MAX_LENGTH)
+}
+
/**
* Lifts a custom block's child-run handle off a tool result onto the tool span,
* the way {@link createBaseSpan} lifts it off a block log.
@@ -251,6 +296,10 @@ function buildChildrenFromTimeSegments(
const { output, handle } = liftChildTraceHandle(
normalizeTraceOutput(match?.result ?? match?.output)
)
+ const errorMessage = getToolCallErrorMessage(match, segment.errorMessage)
+ const errorHandled = Boolean(
+ errorMessage && span.type === 'agent' && span.status === 'success'
+ )
const toolChild: TraceSpan = {
id: `${span.id}-segment-${index}`,
@@ -259,14 +308,15 @@ function buildChildrenFromTimeSegments(
duration: segment.duration,
startTime: segmentStartTime,
endTime: segmentEndTime,
- status: match?.error || segment.errorMessage ? 'error' : 'success',
+ status: errorMessage ? 'error' : 'success',
input: match?.arguments ?? match?.input,
output: match?.error ? { error: match.error, ...output } : output,
+ ...(errorHandled && { errorHandled: true }),
...handle,
}
if (segment.toolCallId) toolChild.toolCallId = segment.toolCallId
if (segment.errorType) toolChild.errorType = segment.errorType
- if (segment.errorMessage) toolChild.errorMessage = segment.errorMessage
+ if (errorMessage) toolChild.errorMessage = errorMessage
return toolChild
}
@@ -330,6 +380,8 @@ function buildChildrenFromToolCalls(span: TraceSpan, log: ValidBlockLog): TraceS
const startTime = tc.startTime ?? log.startedAt
const endTime = tc.endTime ?? log.endedAt
const { output, handle } = liftChildTraceHandle(normalizeTraceOutput(tc.result ?? tc.output))
+ const errorMessage = getToolCallErrorMessage(tc)
+ const errorHandled = Boolean(errorMessage && span.type === 'agent' && span.status === 'success')
return {
id: `${span.id}-tool-${index}`,
name: stripCustomToolPrefix(tc.name ?? 'unnamed-tool'),
@@ -337,9 +389,11 @@ function buildChildrenFromToolCalls(span: TraceSpan, log: ValidBlockLog): TraceS
duration: tc.duration ?? 0,
startTime,
endTime,
- status: tc.error ? 'error' : 'success',
+ status: errorMessage ? 'error' : 'success',
input: tc.arguments ?? tc.input,
output: tc.error ? { error: tc.error, ...output } : output,
+ ...(errorMessage && { errorMessage }),
+ ...(errorHandled && { errorHandled: true }),
...handle,
}
})
diff --git a/apps/sim/lib/workspace-events/constants.ts b/apps/sim/lib/workspace-events/constants.ts
index 12f855dc7f0..87406df585a 100644
--- a/apps/sim/lib/workspace-events/constants.ts
+++ b/apps/sim/lib/workspace-events/constants.ts
@@ -16,6 +16,7 @@ export const SIM_WORKSPACE_EVENT_TRIGGER_ID = 'sim_workspace_event'
export const SIM_PLAIN_EVENT_TYPES = [
'execution_success',
'execution_error',
+ 'agent_tool_error',
'workflow_deployed',
'workflow_undeployed',
] as const
@@ -89,7 +90,10 @@ interface SimEventPayloadField {
/** Restricts which event types surface this field in the tag dropdown. */
condition?: SimEventPayloadFieldCondition
/** Nested fields for json outputs, surfaced as dotted paths in the tag dropdown. */
- properties?: Record
+ properties?: Record<
+ string,
+ { type: 'string' | 'number' | 'json' | 'boolean'; description: string }
+ >
}
/** Run summary fields shared by top-level plain events and the nested triggeringRun. */
@@ -140,7 +144,10 @@ export const SIM_EVENT_PAYLOAD_FIELDS = {
},
runId: {
...RUN_SUMMARY_FIELDS.runId,
- condition: { field: 'eventType', value: [...SIM_PLAIN_RUN_EVENT_TYPES] },
+ condition: {
+ field: 'eventType',
+ value: [...SIM_PLAIN_RUN_EVENT_TYPES, 'agent_tool_error'],
+ },
},
durationMs: {
...RUN_SUMMARY_FIELDS.durationMs,
@@ -160,6 +167,41 @@ export const SIM_EVENT_PAYLOAD_FIELDS = {
condition: { field: 'eventType', value: [...SIM_RUN_BACKED_RULE_EVENT_TYPES] },
properties: RUN_SUMMARY_FIELDS,
},
+ toolError: {
+ type: 'json',
+ description: 'The failed Agent tool invocation',
+ condition: { field: 'eventType', value: 'agent_tool_error' },
+ properties: {
+ agentBlockId: {
+ type: 'string',
+ description: 'The Agent block ID',
+ },
+ agentBlockName: {
+ type: 'string',
+ description: 'The Agent block name',
+ },
+ toolName: {
+ type: 'string',
+ description: 'The failed tool name',
+ },
+ toolCallId: {
+ type: 'string',
+ description: 'The provider tool call ID, when available',
+ },
+ errorMessage: {
+ type: 'string',
+ description: 'The bounded tool failure message',
+ },
+ durationMs: {
+ type: 'number',
+ description: 'Tool invocation duration in milliseconds',
+ },
+ recovered: {
+ type: 'boolean',
+ description: 'Whether the Agent completed successfully after the failure',
+ },
+ },
+ },
version: {
type: 'number',
description: 'The deployment version number that was activated',
diff --git a/apps/sim/lib/workspace-events/emitter.test.ts b/apps/sim/lib/workspace-events/emitter.test.ts
index 4d278728d84..a0816fcba11 100644
--- a/apps/sim/lib/workspace-events/emitter.test.ts
+++ b/apps/sim/lib/workspace-events/emitter.test.ts
@@ -40,7 +40,7 @@ vi.mock('@/lib/workspace-events/state', () => ({
vi.mock('@/lib/webhooks/processor', () => webhooksProcessorMock)
-import type { WorkflowExecutionLog } from '@/lib/logs/types'
+import type { TraceSpan, WorkflowExecutionLog } from '@/lib/logs/types'
import {
emitExecutionCompletedEvent,
emitWorkflowDeployedEvent,
@@ -106,6 +106,45 @@ function makeLog(overrides: Partial = {}): WorkflowExecuti
}
}
+function makeAgentTrace(toolStatus: 'success' | 'error'): TraceSpan[] {
+ const toolSpan: TraceSpan = {
+ id: 'agent-1-segment-1',
+ name: 'always_fail',
+ type: 'tool',
+ duration: 12361,
+ startTime: '2026-08-14T00:08:49.018Z',
+ endTime: '2026-08-14T00:09:01.379Z',
+ status: toolStatus,
+ toolCallId: 'tool-call-1',
+ ...(toolStatus === 'error'
+ ? { errorHandled: true, errorMessage: 'Intentional test tool failure' }
+ : {}),
+ }
+ const agentSpan: TraceSpan = {
+ id: 'agent-1-span',
+ blockId: 'agent-1',
+ name: 'Agent',
+ type: 'agent',
+ duration: 15984,
+ startTime: '2026-08-14T00:08:46.273Z',
+ endTime: '2026-08-14T00:09:02.257Z',
+ status: 'success',
+ children: [toolSpan],
+ }
+ return [
+ {
+ id: 'workflow-execution',
+ name: 'Workflow Execution',
+ type: 'workflow',
+ duration: 15992,
+ startTime: '2026-08-14T00:08:46.265Z',
+ endTime: '2026-08-14T00:09:02.257Z',
+ status: 'success',
+ children: [agentSpan],
+ },
+ ]
+}
+
describe('emitExecutionCompletedEvent', () => {
beforeEach(() => {
mockGetActiveWorkflowContext.mockResolvedValue({
@@ -155,6 +194,58 @@ describe('emitExecutionCompletedEvent', () => {
)
})
+ it('fires one agent_tool_error event alongside execution_success for a recovered failure', async () => {
+ const toolErrorSub = makeSubscription(makeConfig({ eventType: 'agent_tool_error' }), {
+ subscriberWorkflowId: 'wf-tool-error-sub',
+ })
+ const successSub = makeSubscription(makeConfig({ eventType: 'execution_success' }), {
+ subscriberWorkflowId: 'wf-success-sub',
+ })
+ mockFetchSubscriptions.mockResolvedValueOnce([toolErrorSub, successSub])
+
+ await emitExecutionCompletedEvent(
+ makeLog({
+ level: 'info',
+ executionData: {
+ finalOutput: { content: 'I recovered from the tool failure.' },
+ traceSpans: makeAgentTrace('error'),
+ },
+ })
+ )
+
+ expect(mockProcessPolledWebhookEvent).toHaveBeenCalledTimes(2)
+ expect(mockProcessPolledWebhookEvent.mock.calls[0][2]).toMatchObject({
+ event: 'agent_tool_error',
+ workflowId: 'wf-source',
+ workflowName: 'Source Workflow',
+ runId: 'exec-1',
+ toolError: {
+ agentBlockId: 'agent-1',
+ agentBlockName: 'Agent',
+ toolName: 'always_fail',
+ toolCallId: 'tool-call-1',
+ errorMessage: 'Intentional test tool failure',
+ durationMs: 12361,
+ recovered: true,
+ },
+ })
+ expect(mockProcessPolledWebhookEvent.mock.calls[1][2]).toMatchObject({
+ event: 'execution_success',
+ runId: 'exec-1',
+ })
+ })
+
+ it('does not fire agent_tool_error when every Agent tool call succeeded', async () => {
+ const toolErrorSub = makeSubscription(makeConfig({ eventType: 'agent_tool_error' }))
+ mockFetchSubscriptions.mockResolvedValueOnce([toolErrorSub])
+
+ await emitExecutionCompletedEvent(
+ makeLog({ executionData: { traceSpans: makeAgentTrace('success') } })
+ )
+
+ expect(mockProcessPolledWebhookEvent).not.toHaveBeenCalled()
+ })
+
it('respects the workflow scope filter, ignoring stale workflow ids', async () => {
const matching = makeSubscription(makeConfig({ workflowIds: ['wf-source', 'wf-deleted'] }), {
subscriberWorkflowId: 'wf-a',
diff --git a/apps/sim/lib/workspace-events/emitter.ts b/apps/sim/lib/workspace-events/emitter.ts
index ce672aef57a..8d580576463 100644
--- a/apps/sim/lib/workspace-events/emitter.ts
+++ b/apps/sim/lib/workspace-events/emitter.ts
@@ -1,13 +1,14 @@
import { createLogger } from '@sim/logger'
import { getActiveWorkflowContext } from '@sim/platform-authz/workflow'
import { generateShortId } from '@sim/utils/id'
-import type { WorkflowExecutionLog } from '@/lib/logs/types'
+import type { TraceSpan, WorkflowExecutionLog } from '@/lib/logs/types'
import {
isSimRuleEventType,
SIM_RULE_COOLDOWN_HOURS,
SIM_TRIGGER_PROVIDER,
} from '@/lib/workspace-events/constants'
import {
+ buildAgentToolErrorEventPayload,
buildDeployEventPayload,
buildExecutionEventPayload,
buildUndeployEventPayload,
@@ -23,6 +24,7 @@ import type {
SimEventPayload,
SimSubscription,
SimSubscriptionConfig,
+ SimToolError,
} from '@/lib/workspace-events/types'
const logger = createLogger('WorkspaceEventEmitter')
@@ -77,6 +79,33 @@ function matchesWorkflowScope(config: SimSubscriptionConfig, sourceWorkflowId: s
return config.workflowIds.includes(sourceWorkflowId)
}
+/** Collects canonical failed tool spans belonging directly to Agent blocks. */
+function collectAgentToolErrors(spans: TraceSpan[]): SimToolError[] {
+ const toolErrors: SimToolError[] = []
+
+ const visit = (span: TraceSpan): void => {
+ if (span.type === 'agent') {
+ for (const child of span.children ?? []) {
+ if (child.type !== 'tool' || child.status !== 'error' || !child.errorMessage) continue
+ toolErrors.push({
+ agentBlockId: span.blockId ?? span.id,
+ agentBlockName: span.name,
+ toolName: child.name,
+ toolCallId: child.toolCallId ?? null,
+ errorMessage: child.errorMessage,
+ durationMs: child.duration,
+ recovered: span.status === 'success',
+ })
+ }
+ }
+
+ for (const child of span.children ?? []) visit(child)
+ }
+
+ for (const span of spans) visit(span)
+ return toolErrors
+}
+
/**
* Emits workspace events for a completed workflow execution.
*
@@ -96,6 +125,7 @@ export async function emitExecutionCompletedEvent(log: WorkflowExecutionLog): Pr
if (subscriptions.length === 0) return
const executionData = (log.executionData ?? {}) as Record
+ const toolErrors = collectAgentToolErrors(log.executionData.traceSpans ?? [])
const context: ExecutionEventContext = {
workflowId: log.workflowId,
executionId: log.executionId,
@@ -117,6 +147,21 @@ export async function emitExecutionCompletedEvent(log: WorkflowExecutionLog): Pr
if (subscription.webhook.workflowId === log.workflowId) continue
if (!matchesWorkflowScope(config, log.workflowId)) continue
+ if (config.eventType === 'agent_tool_error') {
+ for (const toolError of toolErrors) {
+ await dispatchSimEvent(
+ subscription,
+ buildAgentToolErrorEventPayload({
+ workflowId: log.workflowId,
+ workflowName: workflowContext.workflow.name,
+ runId: log.executionId,
+ toolError,
+ })
+ )
+ }
+ continue
+ }
+
if (config.eventType === 'execution_success' && context.status !== 'success') continue
if (config.eventType === 'execution_error' && context.status !== 'error') continue
diff --git a/apps/sim/lib/workspace-events/payload.ts b/apps/sim/lib/workspace-events/payload.ts
index b05d237c987..a2211838480 100644
--- a/apps/sim/lib/workspace-events/payload.ts
+++ b/apps/sim/lib/workspace-events/payload.ts
@@ -10,6 +10,7 @@ import type {
ExecutionEventContext,
SimEventPayload,
SimRunSummary,
+ SimToolError,
} from '@/lib/workspace-events/types'
/**
@@ -46,10 +47,29 @@ function basePayload(params: {
cost: null,
finalOutput: null,
triggeringRun: null,
+ toolError: null,
version: null,
}
}
+/** Payload for one failed Agent tool invocation discovered at run completion. */
+export function buildAgentToolErrorEventPayload(params: {
+ workflowId: string
+ workflowName: string
+ runId: string
+ toolError: SimToolError
+}): SimEventPayload {
+ return {
+ ...basePayload({
+ event: 'agent_tool_error',
+ workflowId: params.workflowId,
+ workflowName: params.workflowName,
+ }),
+ runId: params.runId,
+ toolError: params.toolError,
+ }
+}
+
/** Run summary in user-facing units: cost in credits, finalOutput bounded. */
function summarizeRun(context: ExecutionEventContext): SimRunSummary {
return {
@@ -67,7 +87,9 @@ function summarizeRun(context: ExecutionEventContext): SimRunSummary {
* the condition that fired, so it nests under `triggeringRun`.
*/
export function buildExecutionEventPayload(params: {
- event: Exclude | SimRuleEventType
+ event:
+ | Exclude
+ | SimRuleEventType
workflowName: string
context: ExecutionEventContext
}): SimEventPayload {
diff --git a/apps/sim/lib/workspace-events/types.ts b/apps/sim/lib/workspace-events/types.ts
index 427894b772b..47211b1339f 100644
--- a/apps/sim/lib/workspace-events/types.ts
+++ b/apps/sim/lib/workspace-events/types.ts
@@ -46,6 +46,17 @@ export interface SimRunSummary {
finalOutput: unknown
}
+/** Safe, bounded metadata describing one failed tool invocation inside an Agent. */
+export interface SimToolError {
+ agentBlockId: string
+ agentBlockName: string
+ toolName: string
+ toolCallId: string | null
+ errorMessage: string
+ durationMs: number
+ recovered: boolean
+}
+
/**
* Wire payload delivered to a Sim trigger workflow. Keys must align with
* SIM_EVENT_PAYLOAD_FIELDS — enforced by tests on the payload builders.
@@ -60,5 +71,6 @@ export type SimEventPayload = Record & {
cost: number | null
finalOutput: unknown
triggeringRun: SimRunSummary | null
+ toolError: SimToolError | null
version: number | null
}
diff --git a/apps/sim/triggers/sim/workspace-event.ts b/apps/sim/triggers/sim/workspace-event.ts
index 1bc9835918c..c400840f93a 100644
--- a/apps/sim/triggers/sim/workspace-event.ts
+++ b/apps/sim/triggers/sim/workspace-event.ts
@@ -24,6 +24,7 @@ export const simWorkspaceEventTrigger: TriggerConfig = {
options: [
{ id: 'execution_error', label: 'Run Error', group: 'Events' },
{ id: 'execution_success', label: 'Run Success', group: 'Events' },
+ { id: 'agent_tool_error', label: 'Agent Tool Error', group: 'Events' },
{ id: 'workflow_deployed', label: 'Workflow Deployed', group: 'Events' },
{ id: 'workflow_undeployed', label: 'Workflow Undeployed', group: 'Events' },
{ id: 'consecutive_failures', label: 'Consecutive Failures', group: 'Alert Conditions' },