Skip to content

Commit 338d695

Browse files
feat(logs): add Logs block for querying execution logs from workflows
1 parent bdaf112 commit 338d695

10 files changed

Lines changed: 512 additions & 11 deletions

File tree

apps/sim/app/api/logs/[id]/route.ts

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import { createLogger } from '@sim/logger'
1010
import { and, eq } from 'drizzle-orm'
1111
import { type NextRequest, NextResponse } from 'next/server'
1212
import { logIdParamsSchema } from '@/lib/api/contracts/logs'
13-
import { getSession } from '@/lib/auth'
13+
import { checkSessionOrInternalAuth } from '@/lib/auth/hybrid'
1414
import { generateRequestId } from '@/lib/core/utils/request'
1515
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
1616

@@ -19,17 +19,20 @@ const logger = createLogger('LogDetailsByIdAPI')
1919
export const revalidate = 0
2020

2121
export const GET = withRouteHandler(
22-
async (_request: NextRequest, { params }: { params: Promise<{ id: string }> }) => {
22+
async (request: NextRequest, { params }: { params: Promise<{ id: string }> }) => {
2323
const requestId = generateRequestId()
2424

2525
try {
26-
const session = await getSession()
27-
if (!session?.user?.id) {
26+
const authResult = await checkSessionOrInternalAuth(request, { requireWorkflowId: false })
27+
if (!authResult.success || !authResult.userId) {
2828
logger.warn(`[${requestId}] Unauthorized log details access attempt`)
29-
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
29+
return NextResponse.json(
30+
{ error: authResult.error || 'Authentication required' },
31+
{ status: 401 }
32+
)
3033
}
3134

32-
const userId = session.user.id
35+
const userId = authResult.userId
3336
const { id } = logIdParamsSchema.parse(await params)
3437

3538
const rows = await db

apps/sim/app/api/logs/route.ts

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ import {
2727
import { type NextRequest, NextResponse } from 'next/server'
2828
import { listLogsQuerySchema } from '@/lib/api/contracts/logs'
2929
import { isZodError } from '@/lib/api/server'
30-
import { getSession } from '@/lib/auth'
30+
import { checkSessionOrInternalAuth } from '@/lib/auth/hybrid'
3131
import { generateRequestId } from '@/lib/core/utils/request'
3232
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
3333
import { buildFilterConditions } from '@/lib/logs/filters'
@@ -40,13 +40,16 @@ export const GET = withRouteHandler(async (request: NextRequest) => {
4040
const requestId = generateRequestId()
4141

4242
try {
43-
const session = await getSession()
44-
if (!session?.user?.id) {
43+
const authResult = await checkSessionOrInternalAuth(request, { requireWorkflowId: false })
44+
if (!authResult.success || !authResult.userId) {
4545
logger.warn(`[${requestId}] Unauthorized logs access attempt`)
46-
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
46+
return NextResponse.json(
47+
{ error: authResult.error || 'Authentication required' },
48+
{ status: 401 }
49+
)
4750
}
4851

49-
const userId = session.user.id
52+
const userId = authResult.userId
5053

5154
try {
5255
const { searchParams } = new URL(request.url)

apps/sim/blocks/blocks/logs.ts

Lines changed: 232 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,232 @@
1+
import { Library } from '@/components/emcn/icons'
2+
import type { BlockConfig } from '@/blocks/types'
3+
4+
export const LogsBlock: BlockConfig = {
5+
type: 'logs',
6+
name: 'Logs',
7+
description: 'Query workflow execution logs',
8+
longDescription:
9+
'Search workflow execution logs in the current workspace, fetch a single log by id, or load full execution details with the per-block state snapshot.',
10+
bgColor: '#EAB308',
11+
bestPractices: `
12+
- The block always operates on the current workspace; you cannot query other workspaces.
13+
- 'Query Logs' returns metadata only by default. Switch the Detail Level to 'Full' only when you specifically need executionData and trace spans — those payloads can be large.
14+
- Use 'Get Execution Details' (with an executionId) to inspect per-block state for a single run.
15+
`,
16+
icon: Library,
17+
category: 'blocks',
18+
docsLink: 'https://docs.sim.ai/api-reference/logs/getExecutionDetails',
19+
subBlocks: [
20+
{
21+
id: 'operation',
22+
title: 'Operation',
23+
type: 'dropdown',
24+
options: [
25+
{ label: 'Query Logs', id: 'query' },
26+
{ label: 'Get Log by ID', id: 'get_log' },
27+
{ label: 'Get Execution Details', id: 'get_execution' },
28+
],
29+
placeholder: 'Select operation',
30+
value: () => 'query',
31+
},
32+
{
33+
id: 'workflowIds',
34+
title: 'Workflow IDs',
35+
type: 'short-input',
36+
placeholder: 'Comma-separated workflow IDs',
37+
condition: { field: 'operation', value: 'query' },
38+
},
39+
{
40+
id: 'executionId',
41+
title: 'Execution ID',
42+
type: 'short-input',
43+
placeholder: 'Filter by a single execution ID',
44+
condition: { field: 'operation', value: 'query' },
45+
},
46+
{
47+
id: 'level',
48+
title: 'Level',
49+
type: 'dropdown',
50+
options: [
51+
{ label: 'All', id: 'all' },
52+
{ label: 'Info', id: 'info' },
53+
{ label: 'Error', id: 'error' },
54+
{ label: 'Running', id: 'running' },
55+
{ label: 'Pending', id: 'pending' },
56+
],
57+
value: () => 'all',
58+
condition: { field: 'operation', value: 'query' },
59+
},
60+
{
61+
id: 'triggers',
62+
title: 'Triggers',
63+
type: 'short-input',
64+
placeholder: 'api,webhook,schedule,manual,chat,mothership',
65+
condition: { field: 'operation', value: 'query' },
66+
},
67+
{
68+
id: 'limit',
69+
title: 'Limit',
70+
type: 'short-input',
71+
placeholder: '100',
72+
condition: { field: 'operation', value: 'query' },
73+
},
74+
{
75+
id: 'startDate',
76+
title: 'Start Date',
77+
type: 'short-input',
78+
placeholder: 'ISO 8601 timestamp',
79+
mode: 'advanced',
80+
wandConfig: {
81+
enabled: true,
82+
prompt:
83+
'Generate an ISO 8601 timestamp from the user description. Return ONLY the timestamp string.',
84+
generationType: 'timestamp',
85+
},
86+
condition: { field: 'operation', value: 'query' },
87+
},
88+
{
89+
id: 'endDate',
90+
title: 'End Date',
91+
type: 'short-input',
92+
placeholder: 'ISO 8601 timestamp',
93+
mode: 'advanced',
94+
wandConfig: {
95+
enabled: true,
96+
prompt:
97+
'Generate an ISO 8601 timestamp from the user description. Return ONLY the timestamp string.',
98+
generationType: 'timestamp',
99+
},
100+
condition: { field: 'operation', value: 'query' },
101+
},
102+
{
103+
id: 'search',
104+
title: 'Search',
105+
type: 'short-input',
106+
placeholder: 'Free-text search',
107+
mode: 'advanced',
108+
condition: { field: 'operation', value: 'query' },
109+
},
110+
{
111+
id: 'details',
112+
title: 'Detail Level',
113+
type: 'dropdown',
114+
options: [
115+
{ label: 'Basic', id: 'basic' },
116+
{ label: 'Full (includes executionData)', id: 'full' },
117+
],
118+
value: () => 'basic',
119+
mode: 'advanced',
120+
condition: { field: 'operation', value: 'query' },
121+
},
122+
{
123+
id: 'logId',
124+
title: 'Log ID',
125+
type: 'short-input',
126+
placeholder: 'Log entry ID',
127+
condition: { field: 'operation', value: 'get_log' },
128+
required: true,
129+
},
130+
{
131+
id: 'executionIdLookup',
132+
title: 'Execution ID',
133+
type: 'short-input',
134+
placeholder: 'Execution ID',
135+
condition: { field: 'operation', value: 'get_execution' },
136+
required: true,
137+
},
138+
],
139+
tools: {
140+
access: ['logs_query', 'logs_get', 'logs_get_execution'],
141+
config: {
142+
tool: (params: Record<string, any>) => {
143+
const operation = params.operation || 'query'
144+
if (operation === 'get_log') return 'logs_get'
145+
if (operation === 'get_execution') return 'logs_get_execution'
146+
return 'logs_query'
147+
},
148+
params: (params: Record<string, any>) => {
149+
const operation = params.operation || 'query'
150+
151+
if (operation === 'get_log') {
152+
if (!params.logId) {
153+
throw new Error('Logs Block Error: Log ID is required for get_log operation')
154+
}
155+
return { id: params.logId }
156+
}
157+
158+
if (operation === 'get_execution') {
159+
if (!params.executionIdLookup) {
160+
throw new Error(
161+
'Logs Block Error: Execution ID is required for get_execution operation'
162+
)
163+
}
164+
return { executionId: params.executionIdLookup }
165+
}
166+
167+
const rawLimit =
168+
params.limit !== undefined && params.limit !== null && params.limit !== ''
169+
? Number(params.limit)
170+
: undefined
171+
const userLimit = Number.isFinite(rawLimit) ? rawLimit : undefined
172+
const isFullDetails = params.details === 'full'
173+
const FULL_DETAILS_MAX = 10
174+
const limit = isFullDetails
175+
? Math.min(userLimit ?? FULL_DETAILS_MAX, FULL_DETAILS_MAX)
176+
: userLimit
177+
178+
return {
179+
workflowIds: params.workflowIds || undefined,
180+
executionId: params.executionId || undefined,
181+
level: params.level && params.level !== 'all' ? params.level : undefined,
182+
triggers: params.triggers || undefined,
183+
limit,
184+
startDate: params.startDate || undefined,
185+
endDate: params.endDate || undefined,
186+
search: params.search || undefined,
187+
details: params.details || undefined,
188+
}
189+
},
190+
},
191+
},
192+
inputs: {
193+
operation: { type: 'string', description: 'Operation to perform' },
194+
workflowIds: { type: 'string', description: 'Comma-separated workflow IDs' },
195+
executionId: { type: 'string', description: 'Execution ID filter (query operation)' },
196+
level: { type: 'string', description: 'Log level filter' },
197+
triggers: { type: 'string', description: 'Comma-separated triggers' },
198+
limit: { type: 'number', description: 'Max logs to return' },
199+
startDate: { type: 'string', description: 'ISO 8601 lower bound' },
200+
endDate: { type: 'string', description: 'ISO 8601 upper bound' },
201+
search: { type: 'string', description: 'Free-text search term' },
202+
details: { type: 'string', description: "'basic' or 'full'" },
203+
logId: { type: 'string', description: 'Log entry ID (get_log operation)' },
204+
executionIdLookup: {
205+
type: 'string',
206+
description: 'Execution ID (get_execution operation)',
207+
},
208+
},
209+
outputs: {
210+
logs: { type: 'json', description: 'Array of log entries (query operation)' },
211+
total: { type: 'number', description: 'Total matching logs (query operation)' },
212+
page: { type: 'number', description: 'Current page (query operation)' },
213+
pageSize: { type: 'number', description: 'Page size (query operation)' },
214+
totalPages: { type: 'number', description: 'Total pages (query operation)' },
215+
log: { type: 'json', description: 'Single log entry (get_log operation)' },
216+
executionId: { type: 'string', description: 'Execution ID (get_execution operation)' },
217+
workflowId: { type: 'string', description: 'Workflow ID (get_execution operation)' },
218+
workflowState: {
219+
type: 'json',
220+
description: 'Per-block state snapshot (get_execution operation)',
221+
},
222+
childWorkflowSnapshots: {
223+
type: 'json',
224+
description: 'Snapshots for child workflows (get_execution operation)',
225+
},
226+
executionMetadata: {
227+
type: 'json',
228+
description:
229+
'Trigger, timestamps, totalDurationMs, cost, totalTokens (get_execution operation)',
230+
},
231+
},
232+
}

apps/sim/blocks/registry.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,7 @@ import { LemlistBlock } from '@/blocks/blocks/lemlist'
113113
import { LinearBlock, LinearV2Block } from '@/blocks/blocks/linear'
114114
import { LinkedInBlock } from '@/blocks/blocks/linkedin'
115115
import { LinkupBlock } from '@/blocks/blocks/linkup'
116+
import { LogsBlock } from '@/blocks/blocks/logs'
116117
import { LoopsBlock } from '@/blocks/blocks/loops'
117118
import { LumaBlock } from '@/blocks/blocks/luma'
118119
import { MailchimpBlock } from '@/blocks/blocks/mailchimp'
@@ -361,6 +362,7 @@ export const registry: Record<string, BlockConfig> = {
361362
linear_v2: LinearV2Block,
362363
linkedin: LinkedInBlock,
363364
linkup: LinkupBlock,
365+
logs: LogsBlock,
364366
loops: LoopsBlock,
365367
luma: LumaBlock,
366368
mailchimp: MailchimpBlock,
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import type { LogsGetExecutionParams, LogsGetExecutionResponse } from '@/tools/logs/types'
2+
import type { ToolConfig } from '@/tools/types'
3+
4+
export const logsGetExecutionTool: ToolConfig<LogsGetExecutionParams, LogsGetExecutionResponse> = {
5+
id: 'logs_get_execution',
6+
name: 'Get Execution Details',
7+
description:
8+
'Fetch full execution details for a workflow run, including the per-block state snapshot.',
9+
version: '1.0.0',
10+
11+
params: {
12+
executionId: {
13+
type: 'string',
14+
required: true,
15+
visibility: 'user-or-llm',
16+
description: 'Execution ID returned by a workflow run',
17+
},
18+
},
19+
20+
request: {
21+
url: (params) => `/api/logs/execution/${encodeURIComponent(params.executionId)}`,
22+
method: 'GET',
23+
headers: () => ({
24+
'Content-Type': 'application/json',
25+
}),
26+
},
27+
28+
transformResponse: async (response): Promise<LogsGetExecutionResponse> => {
29+
const data = await response.json()
30+
return {
31+
success: true,
32+
output: data,
33+
}
34+
},
35+
36+
outputs: {
37+
executionId: { type: 'string', description: 'Execution ID' },
38+
workflowId: { type: 'string', description: 'Workflow ID this execution belongs to' },
39+
workflowState: { type: 'json', description: 'Per-block state snapshot for the execution' },
40+
childWorkflowSnapshots: {
41+
type: 'json',
42+
description: 'Snapshots for any child workflows invoked during the run',
43+
optional: true,
44+
},
45+
executionMetadata: {
46+
type: 'json',
47+
description: 'Trigger, timestamps, totalDurationMs, cost, and totalTokens for the run',
48+
},
49+
},
50+
}

0 commit comments

Comments
 (0)