From d53bd51def350a42348337abb7ef99402d0a9c2f Mon Sep 17 00:00:00 2001 From: Theodore Li Date: Wed, 5 Aug 2026 13:18:56 -0700 Subject: [PATCH 1/8] improvement(api): replace workflow jobs with execution resources --- .../docs/de/api-reference/getting-started.mdx | 14 +- .../content/docs/de/api-reference/python.mdx | 66 +++++---- .../docs/de/api-reference/typescript.mdx | 71 +++++---- apps/docs/content/docs/de/sdks/python.mdx | 66 +++++---- apps/docs/content/docs/de/sdks/typescript.mdx | 71 +++++---- .../(generated)/execution/meta.json | 2 +- .../docs/en/api-reference/getting-started.mdx | 14 +- .../content/docs/en/api-reference/python.mdx | 57 +++---- .../docs/en/api-reference/typescript.mdx | 59 ++++---- .../en/workflows/blocks/human-in-the-loop.mdx | 9 +- .../docs/en/workflows/deployment/api.mdx | 58 ++++---- .../docs/es/api-reference/getting-started.mdx | 14 +- .../content/docs/es/api-reference/python.mdx | 66 +++++---- .../docs/es/api-reference/typescript.mdx | 71 +++++---- apps/docs/content/docs/es/sdks/python.mdx | 66 +++++---- apps/docs/content/docs/es/sdks/typescript.mdx | 71 +++++---- .../docs/fr/api-reference/getting-started.mdx | 14 +- .../content/docs/fr/api-reference/python.mdx | 66 +++++---- .../docs/fr/api-reference/typescript.mdx | 71 +++++---- apps/docs/content/docs/fr/sdks/python.mdx | 66 +++++---- apps/docs/content/docs/fr/sdks/typescript.mdx | 71 +++++---- .../docs/ja/api-reference/getting-started.mdx | 14 +- .../content/docs/ja/api-reference/python.mdx | 66 +++++---- .../docs/ja/api-reference/typescript.mdx | 71 +++++---- apps/docs/content/docs/ja/sdks/python.mdx | 66 +++++---- apps/docs/content/docs/ja/sdks/typescript.mdx | 71 +++++---- .../docs/zh/api-reference/getting-started.mdx | 14 +- .../content/docs/zh/api-reference/python.mdx | 59 ++++---- .../docs/zh/api-reference/typescript.mdx | 62 ++++---- apps/docs/content/docs/zh/sdks/python.mdx | 66 +++++---- apps/docs/content/docs/zh/sdks/typescript.mdx | 71 +++++---- apps/docs/openapi-core.json | 139 +----------------- apps/docs/openapi.json | 139 +----------------- apps/sim/app/api/jobs/[jobId]/route.test.ts | 102 ------------- apps/sim/app/api/jobs/[jobId]/route.ts | 90 ------------ .../[executionId]/[contextId]/route.test.ts | 48 +++++- .../[executionId]/[contextId]/route.ts | 11 +- .../executions/[executionId]/route.test.ts | 38 ++--- .../[id]/executions/[executionId]/route.ts | 79 ++-------- .../[id]/execute/route.async.test.ts | 5 +- .../app/api/workflows/[id]/execute/route.ts | 3 +- .../deploy-modal/components/api/api.tsx | 31 ++-- apps/sim/lib/api/contracts/common.ts | 24 --- apps/sim/lib/api/contracts/primitives.ts | 4 - apps/sim/lib/api/contracts/workflows.ts | 2 +- apps/sim/lib/compare/data/sim.ts | 10 +- .../tools/handlers/deployment/deploy.ts | 19 ++- .../workflows/executor/enqueue-execution.ts | 1 + .../executor/execution-status.test.ts | 90 ++++++++++++ .../workflows/executor/execution-status.ts | 45 +++++- packages/python-sdk/README.md | 23 ++- packages/python-sdk/simstudio/__init__.py | 41 ++++-- packages/python-sdk/tests/test_client.py | 53 +++---- packages/ts-sdk/README.md | 25 ++-- packages/ts-sdk/src/index.test.ts | 53 ++++--- packages/ts-sdk/src/index.ts | 54 +++++-- 56 files changed, 1325 insertions(+), 1427 deletions(-) delete mode 100644 apps/sim/app/api/jobs/[jobId]/route.test.ts delete mode 100644 apps/sim/app/api/jobs/[jobId]/route.ts create mode 100644 apps/sim/lib/workflows/executor/execution-status.test.ts diff --git a/apps/docs/content/docs/de/api-reference/getting-started.mdx b/apps/docs/content/docs/de/api-reference/getting-started.mdx index 7e94ab0d7bd..fec1ab44c46 100644 --- a/apps/docs/content/docs/de/api-reference/getting-started.mdx +++ b/apps/docs/content/docs/de/api-reference/getting-started.mdx @@ -109,27 +109,27 @@ curl -X POST https://www.sim.ai/api/workflows/{workflowId}/execute \ -d '{"inputs": {}, "async": true}' ``` -This returns immediately with a `jobId` and `statusUrl`: +This returns immediately with an `executionId` and `statusUrl`: ```json { "success": true, - "jobId": "job_abc123", - "statusUrl": "https://www.sim.ai/api/jobs/job_abc123", - "message": "Workflow execution started", + "executionId": "c7a92e15-3f4b-4d8c-a1e6-9b0d5f2c8e74", + "statusUrl": "https://www.sim.ai/api/workflows/{workflowId}/executions/c7a92e15-3f4b-4d8c-a1e6-9b0d5f2c8e74", + "message": "Workflow execution queued", "async": true } ``` -Poll the [Get Job Status](/api-reference/execution/getJobStatus) endpoint until the status is `completed` or `failed`: +Poll the [Get Execution Status](/api-reference/execution/getWorkflowExecution) endpoint until the status is terminal: ```bash -curl https://www.sim.ai/api/jobs/{jobId} \ +curl https://www.sim.ai/api/workflows/{workflowId}/executions/{executionId}?includeOutput=true \ -H "X-API-Key: YOUR_API_KEY" ``` - Job status transitions follow: `queued` → `processing` → `completed` or `failed`. The `output` field is only present when status is `completed`. + Execution status transitions follow: `queued` → `running` → `completed`, `failed`, `cancelled`, or `paused`. The `finalOutput` field is populated for completed executions when `includeOutput=true`. ## Response Format diff --git a/apps/docs/content/docs/de/api-reference/python.mdx b/apps/docs/content/docs/de/api-reference/python.mdx index 64e1370f87d..76f220da394 100644 --- a/apps/docs/content/docs/de/api-reference/python.mdx +++ b/apps/docs/content/docs/de/api-reference/python.mdx @@ -112,30 +112,34 @@ if is_ready: **Rückgabe:** `bool` -##### get_job_status() +##### get_workflow_execution() -Ruft den Status einer asynchronen Job-Ausführung ab. +Get the status and optional outputs of a workflow execution. ```python -status = client.get_job_status("task-id-from-async-execution") -print("Status:", status["status"]) # 'queued', 'processing', 'completed', 'failed' +status = client.get_workflow_execution("workflow-id", "execution-id", include_output=True) +print("Status:", status["status"]) # 'queued', 'running', 'completed', 'failed' if status["status"] == "completed": - print("Output:", status["output"]) + print("Output:", status["finalOutput"]) ``` -**Parameter:** -- `task_id` (str): Die Task-ID, die von der asynchronen Ausführung zurückgegeben wurde +**Parameters:** +- `workflow_id` (str): The workflow ID +- `execution_id` (str): The execution ID returned from async execution +- `include_output` (bool, optional): Include the final output for completed executions +- `selected_outputs` (list[str], optional): Block output selectors to include -**Rückgabe:** `Dict[str, Any]` +**Returns:** `Dict[str, Any]` -**Antwortfelder:** -- `success` (bool): Ob die Anfrage erfolgreich war -- `taskId` (str): Die Task-ID -- `status` (str): Einer von `'queued'`, `'processing'`, `'completed'`, `'failed'`, `'cancelled'` -- `metadata` (dict): Enthält `startedAt`, `completedAt` und `duration` -- `output` (any, optional): Die Workflow-Ausgabe (wenn abgeschlossen) -- `error` (any, optional): Fehlerdetails (wenn fehlgeschlagen) -- `estimatedDuration` (int, optional): Geschätzte Dauer in Millisekunden (wenn in Bearbeitung/in Warteschlange) +**Response fields:** +- `executionId` (str): The execution ID +- `workflowId` (str): The workflow ID +- `status` (str): One of `'queued'`, `'pending'`, `'running'`, `'paused'`, `'completed'`, `'failed'`, `'cancelled'` +- `startedAt` / `endedAt` (str): Execution timestamps +- `totalDurationMs` (int, optional): Duration in milliseconds +- `finalOutput` (any, optional): The workflow output when requested for a completed execution +- `blockOutputs` (dict, optional): Requested block outputs +- `error` (str, optional): Failure details ##### execute_with_retry() @@ -271,10 +275,10 @@ class WorkflowExecutionResult: @dataclass class AsyncExecutionResult: success: bool - task_id: str - status: str # 'queued' - created_at: str - links: Dict[str, str] # e.g., {"status": "/api/jobs/{taskId}"} + execution_id: str + status_url: str + message: str = "" + async_execution: bool = True ``` ### WorkflowStatus @@ -489,27 +493,31 @@ def execute_async(): # Start async execution result = client.execute_workflow( "workflow-id", - input_data={"data": "large dataset"}, + input={"data": "large dataset"}, async_execution=True # Execute asynchronously ) # Check if result is an async execution - if hasattr(result, 'task_id'): - print(f"Task ID: {result.task_id}") - print(f"Status endpoint: {result.links['status']}") + if hasattr(result, 'async_execution') and result.async_execution: + print(f"Execution ID: {result.execution_id}") + print(f"Status endpoint: {result.status_url}") # Poll for completion - status = client.get_job_status(result.task_id) + status = client.get_workflow_execution( + "workflow-id", result.execution_id, include_output=True + ) - while status["status"] in ["queued", "processing"]: + while status["status"] in ["queued", "pending", "running"]: print(f"Current status: {status['status']}") time.sleep(2) # Wait 2 seconds - status = client.get_job_status(result.task_id) + status = client.get_workflow_execution( + "workflow-id", result.execution_id, include_output=True + ) if status["status"] == "completed": print("Workflow completed!") - print(f"Output: {status['output']}") - print(f"Duration: {status['metadata']['duration']}") + print(f"Output: {status['finalOutput']}") + print(f"Duration: {status['totalDurationMs']}") else: print(f"Workflow failed: {status['error']}") diff --git a/apps/docs/content/docs/de/api-reference/typescript.mdx b/apps/docs/content/docs/de/api-reference/typescript.mdx index fed552b8403..060ff165701 100644 --- a/apps/docs/content/docs/de/api-reference/typescript.mdx +++ b/apps/docs/content/docs/de/api-reference/typescript.mdx @@ -133,31 +133,37 @@ if (isReady) { **Rückgabewert:** `Promise` -##### getJobStatus() +##### getWorkflowExecution() -Den Status einer asynchronen Job-Ausführung abrufen. +Get the status and optional outputs of a workflow execution. ```typescript -const status = await client.getJobStatus('task-id-from-async-execution'); -console.log('Status:', status.status); // 'queued', 'processing', 'completed', 'failed' +const status = await client.getWorkflowExecution('workflow-id', 'execution-id', { + includeOutput: true +}); +console.log('Status:', status.status); // 'queued', 'running', 'completed', 'failed' if (status.status === 'completed') { - console.log('Output:', status.output); + console.log('Output:', status.finalOutput); } ``` -**Parameter:** -- `taskId` (string): Die Task-ID, die von der asynchronen Ausführung zurückgegeben wurde +**Parameters:** +- `workflowId` (string): The workflow ID +- `executionId` (string): The execution ID returned from async execution +- `options.includeOutput` (boolean, optional): Include the final output for completed executions +- `options.selectedOutputs` (string[], optional): Block output selectors to include -**Rückgabewert:** `Promise` +**Returns:** `Promise` -**Antwortfelder:** -- `success` (boolean): Ob die Anfrage erfolgreich war -- `taskId` (string): Die Task-ID -- `status` (string): Einer der Werte `'queued'`, `'processing'`, `'completed'`, `'failed'`, `'cancelled'` -- `metadata` (object): Enthält `startedAt`, `completedAt` und `duration` -- `output` (any, optional): Die Workflow-Ausgabe (wenn abgeschlossen) -- `error` (any, optional): Fehlerdetails (wenn fehlgeschlagen) -- `estimatedDuration` (number, optional): Geschätzte Dauer in Millisekunden (wenn in Bearbeitung/in der Warteschlange) +**Response fields:** +- `executionId` (string): The execution ID +- `workflowId` (string): The workflow ID +- `status` (string): One of `'queued'`, `'pending'`, `'running'`, `'paused'`, `'completed'`, `'failed'`, `'cancelled'` +- `startedAt` / `endedAt` (string): Execution timestamps +- `totalDurationMs` (number, nullable): Duration in milliseconds +- `finalOutput` (any, nullable): The workflow output when requested for a completed execution +- `blockOutputs` (object, nullable): Requested block outputs +- `error` (string, nullable): Failure details ##### executeWithRetry() @@ -286,12 +292,10 @@ interface WorkflowExecutionResult { ```typescript interface AsyncExecutionResult { success: boolean; - taskId: string; - status: 'queued'; - createdAt: string; - links: { - status: string; // e.g., "/api/jobs/{taskId}" - }; + executionId: string; + statusUrl: string; + message: string; + async: true; } ``` @@ -791,29 +795,32 @@ const client = new SimStudioClient({ async function executeAsync() { try { // Start async execution - const result = await client.executeWorkflow('workflow-id', { - input: { data: 'large dataset' }, + const result = await client.executeWorkflow('workflow-id', { data: 'large dataset' }, { async: true // Execute asynchronously }); // Check if result is an async execution - if ('taskId' in result) { - console.log('Task ID:', result.taskId); - console.log('Status endpoint:', result.links.status); + if ('async' in result && result.async) { + console.log('Execution ID:', result.executionId); + console.log('Status endpoint:', result.statusUrl); // Poll for completion - let status = await client.getJobStatus(result.taskId); + let status = await client.getWorkflowExecution('workflow-id', result.executionId, { + includeOutput: true + }); - while (status.status === 'queued' || status.status === 'processing') { + while (status.status === 'queued' || status.status === 'pending' || status.status === 'running') { console.log('Current status:', status.status); await new Promise(resolve => setTimeout(resolve, 2000)); // Wait 2 seconds - status = await client.getJobStatus(result.taskId); + status = await client.getWorkflowExecution('workflow-id', result.executionId, { + includeOutput: true + }); } if (status.status === 'completed') { console.log('Workflow completed!'); - console.log('Output:', status.output); - console.log('Duration:', status.metadata.duration); + console.log('Output:', status.finalOutput); + console.log('Duration:', status.totalDurationMs); } else { console.error('Workflow failed:', status.error); } diff --git a/apps/docs/content/docs/de/sdks/python.mdx b/apps/docs/content/docs/de/sdks/python.mdx index 64e1370f87d..76f220da394 100644 --- a/apps/docs/content/docs/de/sdks/python.mdx +++ b/apps/docs/content/docs/de/sdks/python.mdx @@ -112,30 +112,34 @@ if is_ready: **Rückgabe:** `bool` -##### get_job_status() +##### get_workflow_execution() -Ruft den Status einer asynchronen Job-Ausführung ab. +Get the status and optional outputs of a workflow execution. ```python -status = client.get_job_status("task-id-from-async-execution") -print("Status:", status["status"]) # 'queued', 'processing', 'completed', 'failed' +status = client.get_workflow_execution("workflow-id", "execution-id", include_output=True) +print("Status:", status["status"]) # 'queued', 'running', 'completed', 'failed' if status["status"] == "completed": - print("Output:", status["output"]) + print("Output:", status["finalOutput"]) ``` -**Parameter:** -- `task_id` (str): Die Task-ID, die von der asynchronen Ausführung zurückgegeben wurde +**Parameters:** +- `workflow_id` (str): The workflow ID +- `execution_id` (str): The execution ID returned from async execution +- `include_output` (bool, optional): Include the final output for completed executions +- `selected_outputs` (list[str], optional): Block output selectors to include -**Rückgabe:** `Dict[str, Any]` +**Returns:** `Dict[str, Any]` -**Antwortfelder:** -- `success` (bool): Ob die Anfrage erfolgreich war -- `taskId` (str): Die Task-ID -- `status` (str): Einer von `'queued'`, `'processing'`, `'completed'`, `'failed'`, `'cancelled'` -- `metadata` (dict): Enthält `startedAt`, `completedAt` und `duration` -- `output` (any, optional): Die Workflow-Ausgabe (wenn abgeschlossen) -- `error` (any, optional): Fehlerdetails (wenn fehlgeschlagen) -- `estimatedDuration` (int, optional): Geschätzte Dauer in Millisekunden (wenn in Bearbeitung/in Warteschlange) +**Response fields:** +- `executionId` (str): The execution ID +- `workflowId` (str): The workflow ID +- `status` (str): One of `'queued'`, `'pending'`, `'running'`, `'paused'`, `'completed'`, `'failed'`, `'cancelled'` +- `startedAt` / `endedAt` (str): Execution timestamps +- `totalDurationMs` (int, optional): Duration in milliseconds +- `finalOutput` (any, optional): The workflow output when requested for a completed execution +- `blockOutputs` (dict, optional): Requested block outputs +- `error` (str, optional): Failure details ##### execute_with_retry() @@ -271,10 +275,10 @@ class WorkflowExecutionResult: @dataclass class AsyncExecutionResult: success: bool - task_id: str - status: str # 'queued' - created_at: str - links: Dict[str, str] # e.g., {"status": "/api/jobs/{taskId}"} + execution_id: str + status_url: str + message: str = "" + async_execution: bool = True ``` ### WorkflowStatus @@ -489,27 +493,31 @@ def execute_async(): # Start async execution result = client.execute_workflow( "workflow-id", - input_data={"data": "large dataset"}, + input={"data": "large dataset"}, async_execution=True # Execute asynchronously ) # Check if result is an async execution - if hasattr(result, 'task_id'): - print(f"Task ID: {result.task_id}") - print(f"Status endpoint: {result.links['status']}") + if hasattr(result, 'async_execution') and result.async_execution: + print(f"Execution ID: {result.execution_id}") + print(f"Status endpoint: {result.status_url}") # Poll for completion - status = client.get_job_status(result.task_id) + status = client.get_workflow_execution( + "workflow-id", result.execution_id, include_output=True + ) - while status["status"] in ["queued", "processing"]: + while status["status"] in ["queued", "pending", "running"]: print(f"Current status: {status['status']}") time.sleep(2) # Wait 2 seconds - status = client.get_job_status(result.task_id) + status = client.get_workflow_execution( + "workflow-id", result.execution_id, include_output=True + ) if status["status"] == "completed": print("Workflow completed!") - print(f"Output: {status['output']}") - print(f"Duration: {status['metadata']['duration']}") + print(f"Output: {status['finalOutput']}") + print(f"Duration: {status['totalDurationMs']}") else: print(f"Workflow failed: {status['error']}") diff --git a/apps/docs/content/docs/de/sdks/typescript.mdx b/apps/docs/content/docs/de/sdks/typescript.mdx index fed552b8403..060ff165701 100644 --- a/apps/docs/content/docs/de/sdks/typescript.mdx +++ b/apps/docs/content/docs/de/sdks/typescript.mdx @@ -133,31 +133,37 @@ if (isReady) { **Rückgabewert:** `Promise` -##### getJobStatus() +##### getWorkflowExecution() -Den Status einer asynchronen Job-Ausführung abrufen. +Get the status and optional outputs of a workflow execution. ```typescript -const status = await client.getJobStatus('task-id-from-async-execution'); -console.log('Status:', status.status); // 'queued', 'processing', 'completed', 'failed' +const status = await client.getWorkflowExecution('workflow-id', 'execution-id', { + includeOutput: true +}); +console.log('Status:', status.status); // 'queued', 'running', 'completed', 'failed' if (status.status === 'completed') { - console.log('Output:', status.output); + console.log('Output:', status.finalOutput); } ``` -**Parameter:** -- `taskId` (string): Die Task-ID, die von der asynchronen Ausführung zurückgegeben wurde +**Parameters:** +- `workflowId` (string): The workflow ID +- `executionId` (string): The execution ID returned from async execution +- `options.includeOutput` (boolean, optional): Include the final output for completed executions +- `options.selectedOutputs` (string[], optional): Block output selectors to include -**Rückgabewert:** `Promise` +**Returns:** `Promise` -**Antwortfelder:** -- `success` (boolean): Ob die Anfrage erfolgreich war -- `taskId` (string): Die Task-ID -- `status` (string): Einer der Werte `'queued'`, `'processing'`, `'completed'`, `'failed'`, `'cancelled'` -- `metadata` (object): Enthält `startedAt`, `completedAt` und `duration` -- `output` (any, optional): Die Workflow-Ausgabe (wenn abgeschlossen) -- `error` (any, optional): Fehlerdetails (wenn fehlgeschlagen) -- `estimatedDuration` (number, optional): Geschätzte Dauer in Millisekunden (wenn in Bearbeitung/in der Warteschlange) +**Response fields:** +- `executionId` (string): The execution ID +- `workflowId` (string): The workflow ID +- `status` (string): One of `'queued'`, `'pending'`, `'running'`, `'paused'`, `'completed'`, `'failed'`, `'cancelled'` +- `startedAt` / `endedAt` (string): Execution timestamps +- `totalDurationMs` (number, nullable): Duration in milliseconds +- `finalOutput` (any, nullable): The workflow output when requested for a completed execution +- `blockOutputs` (object, nullable): Requested block outputs +- `error` (string, nullable): Failure details ##### executeWithRetry() @@ -286,12 +292,10 @@ interface WorkflowExecutionResult { ```typescript interface AsyncExecutionResult { success: boolean; - taskId: string; - status: 'queued'; - createdAt: string; - links: { - status: string; // e.g., "/api/jobs/{taskId}" - }; + executionId: string; + statusUrl: string; + message: string; + async: true; } ``` @@ -791,29 +795,32 @@ const client = new SimStudioClient({ async function executeAsync() { try { // Start async execution - const result = await client.executeWorkflow('workflow-id', { - input: { data: 'large dataset' }, + const result = await client.executeWorkflow('workflow-id', { data: 'large dataset' }, { async: true // Execute asynchronously }); // Check if result is an async execution - if ('taskId' in result) { - console.log('Task ID:', result.taskId); - console.log('Status endpoint:', result.links.status); + if ('async' in result && result.async) { + console.log('Execution ID:', result.executionId); + console.log('Status endpoint:', result.statusUrl); // Poll for completion - let status = await client.getJobStatus(result.taskId); + let status = await client.getWorkflowExecution('workflow-id', result.executionId, { + includeOutput: true + }); - while (status.status === 'queued' || status.status === 'processing') { + while (status.status === 'queued' || status.status === 'pending' || status.status === 'running') { console.log('Current status:', status.status); await new Promise(resolve => setTimeout(resolve, 2000)); // Wait 2 seconds - status = await client.getJobStatus(result.taskId); + status = await client.getWorkflowExecution('workflow-id', result.executionId, { + includeOutput: true + }); } if (status.status === 'completed') { console.log('Workflow completed!'); - console.log('Output:', status.output); - console.log('Duration:', status.metadata.duration); + console.log('Output:', status.finalOutput); + console.log('Duration:', status.totalDurationMs); } else { console.error('Workflow failed:', status.error); } diff --git a/apps/docs/content/docs/en/api-reference/(generated)/execution/meta.json b/apps/docs/content/docs/en/api-reference/(generated)/execution/meta.json index 52458d430c3..1a9a9283917 100644 --- a/apps/docs/content/docs/en/api-reference/(generated)/execution/meta.json +++ b/apps/docs/content/docs/en/api-reference/(generated)/execution/meta.json @@ -1,3 +1,3 @@ { - "pages": ["executeWorkflow", "getWorkflowExecution", "cancelExecution", "getJobStatus"] + "pages": ["executeWorkflow", "getWorkflowExecution", "cancelExecution"] } diff --git a/apps/docs/content/docs/en/api-reference/getting-started.mdx b/apps/docs/content/docs/en/api-reference/getting-started.mdx index c8093e72c14..e593d678ff9 100644 --- a/apps/docs/content/docs/en/api-reference/getting-started.mdx +++ b/apps/docs/content/docs/en/api-reference/getting-started.mdx @@ -109,27 +109,27 @@ curl -X POST https://www.sim.ai/api/workflows/{workflowId}/execute \ -d '{"inputs": {}, "async": true}' ``` -This returns immediately with a `jobId` and `statusUrl`: +This returns immediately with an `executionId` and `statusUrl`: ```json { "success": true, - "jobId": "job_abc123", - "statusUrl": "https://www.sim.ai/api/jobs/job_abc123", - "message": "Workflow execution started", + "executionId": "c7a92e15-3f4b-4d8c-a1e6-9b0d5f2c8e74", + "statusUrl": "https://www.sim.ai/api/workflows/{workflowId}/executions/c7a92e15-3f4b-4d8c-a1e6-9b0d5f2c8e74", + "message": "Workflow execution queued", "async": true } ``` -Poll the [Get Job Status](/api-reference/execution/getJobStatus) endpoint until the status is `completed` or `failed`: +Poll the [Get Execution Status](/api-reference/execution/getWorkflowExecution) endpoint until the status is terminal: ```bash -curl https://www.sim.ai/api/jobs/{jobId} \ +curl https://www.sim.ai/api/workflows/{workflowId}/executions/{executionId}?includeOutput=true \ -H "X-API-Key: YOUR_API_KEY" ``` - Job status transitions follow: `queued` → `processing` → `completed` or `failed`. The `output` field is only present when status is `completed`. + Execution status transitions follow: `queued` → `running` → `completed`, `failed`, `cancelled`, or `paused`. The `finalOutput` field is populated for completed executions when `includeOutput=true`. ## Response Format diff --git a/apps/docs/content/docs/en/api-reference/python.mdx b/apps/docs/content/docs/en/api-reference/python.mdx index d70bb50e3aa..caab508b76b 100644 --- a/apps/docs/content/docs/en/api-reference/python.mdx +++ b/apps/docs/content/docs/en/api-reference/python.mdx @@ -80,7 +80,7 @@ result = client.execute_workflow( **Returns:** `WorkflowExecutionResult | AsyncExecutionResult` -When `async_execution=True`, returns immediately with a `job_id` and `status_url` for polling. Otherwise, waits for completion. +When `async_execution=True`, returns immediately with an `execution_id` and `status_url` for polling. Otherwise, waits for completion. ##### get_workflow_status() @@ -112,30 +112,34 @@ if is_ready: **Returns:** `bool` -##### get_job_status() +##### get_workflow_execution() -Get the status of an async job execution. +Get the status and optional outputs of a workflow execution. ```python -status = client.get_job_status("job-id-from-async-execution") -print("Status:", status["status"]) # 'queued', 'processing', 'completed', 'failed' +status = client.get_workflow_execution("workflow-id", "execution-id", include_output=True) +print("Status:", status["status"]) # 'queued', 'running', 'completed', 'failed' if status["status"] == "completed": - print("Output:", status["output"]) + print("Output:", status["finalOutput"]) ``` **Parameters:** -- `task_id` (str): The job ID returned from async execution +- `workflow_id` (str): The workflow ID +- `execution_id` (str): The execution ID returned from async execution +- `include_output` (bool, optional): Include the final output for completed executions +- `selected_outputs` (list[str], optional): Block output selectors to include **Returns:** `Dict[str, Any]` **Response fields:** -- `success` (bool): Whether the request was successful -- `taskId` (str): The job ID -- `status` (str): One of `'queued'`, `'processing'`, `'completed'`, `'failed'`, `'cancelled'` -- `metadata` (dict): Contains `startedAt`, `completedAt`, and `duration` -- `output` (any, optional): The workflow output (when completed) -- `error` (any, optional): Error details (when failed) -- `estimatedDuration` (int, optional): Estimated duration in milliseconds (when processing/queued) +- `executionId` (str): The execution ID +- `workflowId` (str): The workflow ID +- `status` (str): One of `'queued'`, `'pending'`, `'running'`, `'paused'`, `'completed'`, `'failed'`, `'cancelled'` +- `startedAt` / `endedAt` (str): Execution timestamps +- `totalDurationMs` (int, optional): Duration in milliseconds +- `finalOutput` (any, optional): The workflow output when requested for a completed execution +- `blockOutputs` (dict, optional): Requested block outputs +- `error` (str, optional): Failure details ##### execute_with_retry() @@ -270,9 +274,8 @@ class WorkflowExecutionResult: @dataclass class AsyncExecutionResult: success: bool - job_id: str + execution_id: str status_url: str - execution_id: Optional[str] = None message: str = "" async_execution: bool = True ``` @@ -494,22 +497,26 @@ def execute_async(): ) # Check if result is an async execution - if hasattr(result, 'job_id'): - print(f"Job ID: {result.job_id}") + if hasattr(result, 'async_execution') and result.async_execution: + print(f"Execution ID: {result.execution_id}") print(f"Status endpoint: {result.status_url}") # Poll for completion - status = client.get_job_status(result.job_id) + status = client.get_workflow_execution( + "workflow-id", result.execution_id, include_output=True + ) - while status["status"] in ["queued", "processing"]: + while status["status"] in ["queued", "pending", "running"]: print(f"Current status: {status['status']}") time.sleep(2) # Wait 2 seconds - status = client.get_job_status(result.job_id) + status = client.get_workflow_execution( + "workflow-id", result.execution_id, include_output=True + ) if status["status"] == "completed": print("Workflow completed!") - print(f"Output: {status['output']}") - print(f"Duration: {status['metadata']['duration']}") + print(f"Output: {status['finalOutput']}") + print(f"Duration: {status['totalDurationMs']}") else: print(f"Workflow failed: {status['error']}") @@ -765,9 +772,9 @@ import { FAQ } from '@/components/ui/faq' \ No newline at end of file +]} /> diff --git a/apps/docs/content/docs/en/api-reference/typescript.mdx b/apps/docs/content/docs/en/api-reference/typescript.mdx index 791849f94a5..6741c4a6cfb 100644 --- a/apps/docs/content/docs/en/api-reference/typescript.mdx +++ b/apps/docs/content/docs/en/api-reference/typescript.mdx @@ -94,7 +94,7 @@ const result = await client.executeWorkflow('workflow-id', { message: 'Hello, wo **Returns:** `Promise` -When `async: true`, returns immediately with a `jobId` and `statusUrl` for polling. Otherwise, waits for completion. +When `async: true`, returns immediately with an `executionId` and `statusUrl` for polling. Otherwise, waits for completion. ##### getWorkflowStatus() @@ -126,31 +126,37 @@ if (isReady) { **Returns:** `Promise` -##### getJobStatus() +##### getWorkflowExecution() -Get the status of an async job execution. +Get the status and optional outputs of a workflow execution. ```typescript -const status = await client.getJobStatus('job-id-from-async-execution'); -console.log('Status:', status.status); // 'queued', 'processing', 'completed', 'failed' +const status = await client.getWorkflowExecution('workflow-id', 'execution-id', { + includeOutput: true +}); +console.log('Status:', status.status); // 'queued', 'running', 'completed', 'failed' if (status.status === 'completed') { - console.log('Output:', status.output); + console.log('Output:', status.finalOutput); } ``` **Parameters:** -- `jobId` (string): The job ID returned from async execution +- `workflowId` (string): The workflow ID +- `executionId` (string): The execution ID returned from async execution +- `options.includeOutput` (boolean, optional): Include the final output for completed executions +- `options.selectedOutputs` (string[], optional): Block output selectors to include -**Returns:** `Promise` +**Returns:** `Promise` **Response fields:** -- `success` (boolean): Whether the request was successful -- `taskId` (string): The job ID -- `status` (string): One of `'queued'`, `'processing'`, `'completed'`, `'failed'`, `'cancelled'` -- `metadata` (object): Contains `startedAt`, `completedAt`, and `duration` -- `output` (any, optional): The workflow output (when completed) -- `error` (any, optional): Error details (when failed) -- `estimatedDuration` (number, optional): Estimated duration in milliseconds (when processing/queued) +- `executionId` (string): The execution ID +- `workflowId` (string): The workflow ID +- `status` (string): One of `'queued'`, `'pending'`, `'running'`, `'paused'`, `'completed'`, `'failed'`, `'cancelled'` +- `startedAt` / `endedAt` (string): Execution timestamps +- `totalDurationMs` (number, nullable): Duration in milliseconds +- `finalOutput` (any, nullable): The workflow output when requested for a completed execution +- `blockOutputs` (object, nullable): Requested block outputs +- `error` (string, nullable): Failure details ##### executeWithRetry() @@ -278,9 +284,8 @@ interface WorkflowExecutionResult { ```typescript interface AsyncExecutionResult { success: boolean; - jobId: string; + executionId: string; statusUrl: string; - executionId?: string; message: string; async: true; } @@ -766,23 +771,27 @@ async function executeAsync() { }); // Check if result is an async execution - if ('jobId' in result) { - console.log('Job ID:', result.jobId); + if ('async' in result && result.async) { + console.log('Execution ID:', result.executionId); console.log('Status endpoint:', result.statusUrl); // Poll for completion - let status = await client.getJobStatus(result.jobId); + let status = await client.getWorkflowExecution('workflow-id', result.executionId, { + includeOutput: true + }); - while (status.status === 'queued' || status.status === 'processing') { + while (status.status === 'queued' || status.status === 'pending' || status.status === 'running') { console.log('Current status:', status.status); await new Promise(resolve => setTimeout(resolve, 2000)); // Wait 2 seconds - status = await client.getJobStatus(result.jobId); + status = await client.getWorkflowExecution('workflow-id', result.executionId, { + includeOutput: true + }); } if (status.status === 'completed') { console.log('Workflow completed!'); - console.log('Output:', status.output); - console.log('Duration:', status.metadata.duration); + console.log('Output:', status.finalOutput); + console.log('Duration:', status.totalDurationMs); } else { console.error('Workflow failed:', status.error); } @@ -1021,7 +1030,7 @@ import { FAQ } from '@/components/ui/faq' `. - **Stream mode** (`stream: true` on the original execute call) — The resume response streams SSE events with `selectedOutputs` chunks, just like the initial execution. - - **Async mode** (`X-Execution-Mode: async` on the original execute call) — The resume dispatches execution to a background worker and returns immediately with `202`, including a `jobId` and `statusUrl` for polling: + - **Async mode** (`X-Execution-Mode: async` on the original execute call) — The resume dispatches execution to a background worker and returns immediately with `202`, including the resume attempt's `executionId` and `statusUrl` for polling: ```json { "success": true, "async": true, - "jobId": "", "executionId": "", "message": "Resume execution queued", - "statusUrl": "/api/jobs/" + "statusUrl": "/api/workflows//executions/" } ``` @@ -139,11 +138,11 @@ Access resume data in downstream blocks using ``. Poll the `statusUrl` from the async response to check when the resume completes: ```bash - GET /api/jobs/{jobId} + GET /api/workflows/{workflowId}/executions/{resumeExecutionId}?includeOutput=true X-API-Key: your-api-key ``` - Returns job status and, when completed, the full workflow output. + Returns the execution status and, when completed, the full workflow output. To check on a paused execution's pause points and resume links: diff --git a/apps/docs/content/docs/en/workflows/deployment/api.mdx b/apps/docs/content/docs/en/workflows/deployment/api.mdx index 1e3821d5776..0e71726bd06 100644 --- a/apps/docs/content/docs/en/workflows/deployment/api.mdx +++ b/apps/docs/content/docs/en/workflows/deployment/api.mdx @@ -280,10 +280,10 @@ The `version` field is part of the external API contract. Treat the reference as ### Asynchronous -For long-running workflows, async mode returns a job ID immediately so you don't need to hold the connection open. Add the `X-Execution-Mode: async` header to your request. The API returns HTTP 202 with a job ID and status URL. Poll the status URL until the job completes. +For long-running workflows, async mode returns an execution ID immediately so you don't need to hold the connection open. Add the `X-Execution-Mode: async` header to your request. The API returns HTTP 202 with an execution ID and status URL. Poll the execution resource until the run completes. - - + + ```bash curl -X POST https://sim.ai/api/workflows/{workflow-id}/execute \ -H "Content-Type: application/json" \ @@ -297,59 +297,57 @@ curl -X POST https://sim.ai/api/workflows/{workflow-id}/execute \ { "success": true, "async": true, - "jobId": "run_abc123", "executionId": "c7a92e15-3f4b-4d8c-a1e6-9b0d5f2c8e74", "message": "Workflow execution queued", - "statusUrl": "https://sim.ai/api/jobs/run_abc123" + "statusUrl": "https://sim.ai/api/workflows/{workflow-id}/executions/c7a92e15-3f4b-4d8c-a1e6-9b0d5f2c8e74" } ``` ```bash -curl https://sim.ai/api/jobs/{jobId} \ +curl "https://sim.ai/api/workflows/{workflow-id}/executions/{executionId}?includeOutput=true" \ -H "x-api-key: $SIM_API_KEY" ``` **While processing:** ```json { - "success": true, - "taskId": "run_abc123", - "status": "processing", - "metadata": { - "createdAt": "2025-09-10T12:00:00.000Z", - "startedAt": "2025-09-10T12:00:01.000Z" - }, - "estimatedDuration": 300000 + "executionId": "c7a92e15-3f4b-4d8c-a1e6-9b0d5f2c8e74", + "workflowId": "{workflow-id}", + "status": "running", + "startedAt": "2025-09-10T12:00:01.000Z", + "endedAt": null, + "totalDurationMs": null, + "finalOutput": null } ``` **When completed:** ```json { - "success": true, - "taskId": "run_abc123", + "executionId": "c7a92e15-3f4b-4d8c-a1e6-9b0d5f2c8e74", + "workflowId": "{workflow-id}", "status": "completed", - "metadata": { - "createdAt": "2025-09-10T12:00:00.000Z", - "startedAt": "2025-09-10T12:00:01.000Z", - "completedAt": "2025-09-10T12:00:05.000Z", - "duration": 4000 - }, - "output": { "result": "..." } + "startedAt": "2025-09-10T12:00:01.000Z", + "endedAt": "2025-09-10T12:00:05.000Z", + "totalDurationMs": 4000, + "finalOutput": { "result": "..." } } ``` -#### Job Status Values +#### Execution Status Values | Status | Description | |--------|-------------| -| `queued` | Job is waiting to be picked up | -| `processing` | Workflow is actively executing | -| `completed` | Finished successfully — `output` field contains the result | +| `queued` | Execution is waiting to be picked up | +| `pending` | The durable execution record exists but has not started | +| `running` | Workflow is actively executing | +| `paused` | Workflow is waiting for a resume condition or input | +| `completed` | Finished successfully — `finalOutput` is populated when requested | | `failed` | Execution failed — `error` field contains the message | +| `cancelled` | Execution was cancelled | Poll the `statusUrl` from the initial response until the status is `completed` or `failed`. @@ -360,11 +358,11 @@ Poll the `statusUrl` from the initial response until the status is `completed` o | **Community** | 5 minutes | 90 minutes | | **Pro / Max / Team / Enterprise** | 50 minutes | 90 minutes | -If a job exceeds its time limit it is automatically marked as `failed`. +If an execution exceeds its time limit it is automatically marked as `failed`. -#### Job Retention +#### Execution Retention -Completed and failed job results are retained for **24 hours**. After that, the status endpoint returns `404`. Retrieve and store results on your end if you need them longer. +Completed and failed runs are read from execution logs and follow the workspace's execution-log retention policy. #### Capacity Limits diff --git a/apps/docs/content/docs/es/api-reference/getting-started.mdx b/apps/docs/content/docs/es/api-reference/getting-started.mdx index c8093e72c14..e593d678ff9 100644 --- a/apps/docs/content/docs/es/api-reference/getting-started.mdx +++ b/apps/docs/content/docs/es/api-reference/getting-started.mdx @@ -109,27 +109,27 @@ curl -X POST https://www.sim.ai/api/workflows/{workflowId}/execute \ -d '{"inputs": {}, "async": true}' ``` -This returns immediately with a `jobId` and `statusUrl`: +This returns immediately with an `executionId` and `statusUrl`: ```json { "success": true, - "jobId": "job_abc123", - "statusUrl": "https://www.sim.ai/api/jobs/job_abc123", - "message": "Workflow execution started", + "executionId": "c7a92e15-3f4b-4d8c-a1e6-9b0d5f2c8e74", + "statusUrl": "https://www.sim.ai/api/workflows/{workflowId}/executions/c7a92e15-3f4b-4d8c-a1e6-9b0d5f2c8e74", + "message": "Workflow execution queued", "async": true } ``` -Poll the [Get Job Status](/api-reference/execution/getJobStatus) endpoint until the status is `completed` or `failed`: +Poll the [Get Execution Status](/api-reference/execution/getWorkflowExecution) endpoint until the status is terminal: ```bash -curl https://www.sim.ai/api/jobs/{jobId} \ +curl https://www.sim.ai/api/workflows/{workflowId}/executions/{executionId}?includeOutput=true \ -H "X-API-Key: YOUR_API_KEY" ``` - Job status transitions follow: `queued` → `processing` → `completed` or `failed`. The `output` field is only present when status is `completed`. + Execution status transitions follow: `queued` → `running` → `completed`, `failed`, `cancelled`, or `paused`. The `finalOutput` field is populated for completed executions when `includeOutput=true`. ## Response Format diff --git a/apps/docs/content/docs/es/api-reference/python.mdx b/apps/docs/content/docs/es/api-reference/python.mdx index cff0a2468b9..d7f89b25e74 100644 --- a/apps/docs/content/docs/es/api-reference/python.mdx +++ b/apps/docs/content/docs/es/api-reference/python.mdx @@ -112,30 +112,34 @@ if is_ready: **Devuelve:** `bool` -##### get_job_status() +##### get_workflow_execution() -Obtener el estado de una ejecución de trabajo asíncrono. +Get the status and optional outputs of a workflow execution. ```python -status = client.get_job_status("task-id-from-async-execution") -print("Status:", status["status"]) # 'queued', 'processing', 'completed', 'failed' +status = client.get_workflow_execution("workflow-id", "execution-id", include_output=True) +print("Status:", status["status"]) # 'queued', 'running', 'completed', 'failed' if status["status"] == "completed": - print("Output:", status["output"]) + print("Output:", status["finalOutput"]) ``` -**Parámetros:** -- `task_id` (str): El ID de tarea devuelto de la ejecución asíncrona +**Parameters:** +- `workflow_id` (str): The workflow ID +- `execution_id` (str): The execution ID returned from async execution +- `include_output` (bool, optional): Include the final output for completed executions +- `selected_outputs` (list[str], optional): Block output selectors to include -**Devuelve:** `Dict[str, Any]` +**Returns:** `Dict[str, Any]` -**Campos de respuesta:** -- `success` (bool): Si la solicitud fue exitosa -- `taskId` (str): El ID de la tarea -- `status` (str): Uno de `'queued'`, `'processing'`, `'completed'`, `'failed'`, `'cancelled'` -- `metadata` (dict): Contiene `startedAt`, `completedAt`, y `duration` -- `output` (any, opcional): La salida del flujo de trabajo (cuando se completa) -- `error` (any, opcional): Detalles del error (cuando falla) -- `estimatedDuration` (int, opcional): Duración estimada en milisegundos (cuando está procesando/en cola) +**Response fields:** +- `executionId` (str): The execution ID +- `workflowId` (str): The workflow ID +- `status` (str): One of `'queued'`, `'pending'`, `'running'`, `'paused'`, `'completed'`, `'failed'`, `'cancelled'` +- `startedAt` / `endedAt` (str): Execution timestamps +- `totalDurationMs` (int, optional): Duration in milliseconds +- `finalOutput` (any, optional): The workflow output when requested for a completed execution +- `blockOutputs` (dict, optional): Requested block outputs +- `error` (str, optional): Failure details ##### execute_with_retry() @@ -271,10 +275,10 @@ class WorkflowExecutionResult: @dataclass class AsyncExecutionResult: success: bool - task_id: str - status: str # 'queued' - created_at: str - links: Dict[str, str] # e.g., {"status": "/api/jobs/{taskId}"} + execution_id: str + status_url: str + message: str = "" + async_execution: bool = True ``` ### WorkflowStatus @@ -489,27 +493,31 @@ def execute_async(): # Start async execution result = client.execute_workflow( "workflow-id", - input_data={"data": "large dataset"}, + input={"data": "large dataset"}, async_execution=True # Execute asynchronously ) # Check if result is an async execution - if hasattr(result, 'task_id'): - print(f"Task ID: {result.task_id}") - print(f"Status endpoint: {result.links['status']}") + if hasattr(result, 'async_execution') and result.async_execution: + print(f"Execution ID: {result.execution_id}") + print(f"Status endpoint: {result.status_url}") # Poll for completion - status = client.get_job_status(result.task_id) + status = client.get_workflow_execution( + "workflow-id", result.execution_id, include_output=True + ) - while status["status"] in ["queued", "processing"]: + while status["status"] in ["queued", "pending", "running"]: print(f"Current status: {status['status']}") time.sleep(2) # Wait 2 seconds - status = client.get_job_status(result.task_id) + status = client.get_workflow_execution( + "workflow-id", result.execution_id, include_output=True + ) if status["status"] == "completed": print("Workflow completed!") - print(f"Output: {status['output']}") - print(f"Duration: {status['metadata']['duration']}") + print(f"Output: {status['finalOutput']}") + print(f"Duration: {status['totalDurationMs']}") else: print(f"Workflow failed: {status['error']}") diff --git a/apps/docs/content/docs/es/api-reference/typescript.mdx b/apps/docs/content/docs/es/api-reference/typescript.mdx index 58c3578c219..40fb802636d 100644 --- a/apps/docs/content/docs/es/api-reference/typescript.mdx +++ b/apps/docs/content/docs/es/api-reference/typescript.mdx @@ -133,31 +133,37 @@ if (isReady) { **Devuelve:** `Promise` -##### getJobStatus() +##### getWorkflowExecution() -Obtener el estado de una ejecución de trabajo asíncrono. +Get the status and optional outputs of a workflow execution. ```typescript -const status = await client.getJobStatus('task-id-from-async-execution'); -console.log('Status:', status.status); // 'queued', 'processing', 'completed', 'failed' +const status = await client.getWorkflowExecution('workflow-id', 'execution-id', { + includeOutput: true +}); +console.log('Status:', status.status); // 'queued', 'running', 'completed', 'failed' if (status.status === 'completed') { - console.log('Output:', status.output); + console.log('Output:', status.finalOutput); } ``` -**Parámetros:** -- `taskId` (string): El ID de tarea devuelto por la ejecución asíncrona +**Parameters:** +- `workflowId` (string): The workflow ID +- `executionId` (string): The execution ID returned from async execution +- `options.includeOutput` (boolean, optional): Include the final output for completed executions +- `options.selectedOutputs` (string[], optional): Block output selectors to include -**Devuelve:** `Promise` +**Returns:** `Promise` -**Campos de respuesta:** -- `success` (boolean): Si la solicitud fue exitosa -- `taskId` (string): El ID de la tarea -- `status` (string): Uno de `'queued'`, `'processing'`, `'completed'`, `'failed'`, `'cancelled'` -- `metadata` (object): Contiene `startedAt`, `completedAt`, y `duration` -- `output` (any, opcional): La salida del flujo de trabajo (cuando se completa) -- `error` (any, opcional): Detalles del error (cuando falla) -- `estimatedDuration` (number, opcional): Duración estimada en milisegundos (cuando está procesando/en cola) +**Response fields:** +- `executionId` (string): The execution ID +- `workflowId` (string): The workflow ID +- `status` (string): One of `'queued'`, `'pending'`, `'running'`, `'paused'`, `'completed'`, `'failed'`, `'cancelled'` +- `startedAt` / `endedAt` (string): Execution timestamps +- `totalDurationMs` (number, nullable): Duration in milliseconds +- `finalOutput` (any, nullable): The workflow output when requested for a completed execution +- `blockOutputs` (object, nullable): Requested block outputs +- `error` (string, nullable): Failure details ##### executeWithRetry() @@ -286,12 +292,10 @@ interface WorkflowExecutionResult { ```typescript interface AsyncExecutionResult { success: boolean; - taskId: string; - status: 'queued'; - createdAt: string; - links: { - status: string; // e.g., "/api/jobs/{taskId}" - }; + executionId: string; + statusUrl: string; + message: string; + async: true; } ``` @@ -791,29 +795,32 @@ const client = new SimStudioClient({ async function executeAsync() { try { // Start async execution - const result = await client.executeWorkflow('workflow-id', { - input: { data: 'large dataset' }, + const result = await client.executeWorkflow('workflow-id', { data: 'large dataset' }, { async: true // Execute asynchronously }); // Check if result is an async execution - if ('taskId' in result) { - console.log('Task ID:', result.taskId); - console.log('Status endpoint:', result.links.status); + if ('async' in result && result.async) { + console.log('Execution ID:', result.executionId); + console.log('Status endpoint:', result.statusUrl); // Poll for completion - let status = await client.getJobStatus(result.taskId); + let status = await client.getWorkflowExecution('workflow-id', result.executionId, { + includeOutput: true + }); - while (status.status === 'queued' || status.status === 'processing') { + while (status.status === 'queued' || status.status === 'pending' || status.status === 'running') { console.log('Current status:', status.status); await new Promise(resolve => setTimeout(resolve, 2000)); // Wait 2 seconds - status = await client.getJobStatus(result.taskId); + status = await client.getWorkflowExecution('workflow-id', result.executionId, { + includeOutput: true + }); } if (status.status === 'completed') { console.log('Workflow completed!'); - console.log('Output:', status.output); - console.log('Duration:', status.metadata.duration); + console.log('Output:', status.finalOutput); + console.log('Duration:', status.totalDurationMs); } else { console.error('Workflow failed:', status.error); } diff --git a/apps/docs/content/docs/es/sdks/python.mdx b/apps/docs/content/docs/es/sdks/python.mdx index cff0a2468b9..d7f89b25e74 100644 --- a/apps/docs/content/docs/es/sdks/python.mdx +++ b/apps/docs/content/docs/es/sdks/python.mdx @@ -112,30 +112,34 @@ if is_ready: **Devuelve:** `bool` -##### get_job_status() +##### get_workflow_execution() -Obtener el estado de una ejecución de trabajo asíncrono. +Get the status and optional outputs of a workflow execution. ```python -status = client.get_job_status("task-id-from-async-execution") -print("Status:", status["status"]) # 'queued', 'processing', 'completed', 'failed' +status = client.get_workflow_execution("workflow-id", "execution-id", include_output=True) +print("Status:", status["status"]) # 'queued', 'running', 'completed', 'failed' if status["status"] == "completed": - print("Output:", status["output"]) + print("Output:", status["finalOutput"]) ``` -**Parámetros:** -- `task_id` (str): El ID de tarea devuelto de la ejecución asíncrona +**Parameters:** +- `workflow_id` (str): The workflow ID +- `execution_id` (str): The execution ID returned from async execution +- `include_output` (bool, optional): Include the final output for completed executions +- `selected_outputs` (list[str], optional): Block output selectors to include -**Devuelve:** `Dict[str, Any]` +**Returns:** `Dict[str, Any]` -**Campos de respuesta:** -- `success` (bool): Si la solicitud fue exitosa -- `taskId` (str): El ID de la tarea -- `status` (str): Uno de `'queued'`, `'processing'`, `'completed'`, `'failed'`, `'cancelled'` -- `metadata` (dict): Contiene `startedAt`, `completedAt`, y `duration` -- `output` (any, opcional): La salida del flujo de trabajo (cuando se completa) -- `error` (any, opcional): Detalles del error (cuando falla) -- `estimatedDuration` (int, opcional): Duración estimada en milisegundos (cuando está procesando/en cola) +**Response fields:** +- `executionId` (str): The execution ID +- `workflowId` (str): The workflow ID +- `status` (str): One of `'queued'`, `'pending'`, `'running'`, `'paused'`, `'completed'`, `'failed'`, `'cancelled'` +- `startedAt` / `endedAt` (str): Execution timestamps +- `totalDurationMs` (int, optional): Duration in milliseconds +- `finalOutput` (any, optional): The workflow output when requested for a completed execution +- `blockOutputs` (dict, optional): Requested block outputs +- `error` (str, optional): Failure details ##### execute_with_retry() @@ -271,10 +275,10 @@ class WorkflowExecutionResult: @dataclass class AsyncExecutionResult: success: bool - task_id: str - status: str # 'queued' - created_at: str - links: Dict[str, str] # e.g., {"status": "/api/jobs/{taskId}"} + execution_id: str + status_url: str + message: str = "" + async_execution: bool = True ``` ### WorkflowStatus @@ -489,27 +493,31 @@ def execute_async(): # Start async execution result = client.execute_workflow( "workflow-id", - input_data={"data": "large dataset"}, + input={"data": "large dataset"}, async_execution=True # Execute asynchronously ) # Check if result is an async execution - if hasattr(result, 'task_id'): - print(f"Task ID: {result.task_id}") - print(f"Status endpoint: {result.links['status']}") + if hasattr(result, 'async_execution') and result.async_execution: + print(f"Execution ID: {result.execution_id}") + print(f"Status endpoint: {result.status_url}") # Poll for completion - status = client.get_job_status(result.task_id) + status = client.get_workflow_execution( + "workflow-id", result.execution_id, include_output=True + ) - while status["status"] in ["queued", "processing"]: + while status["status"] in ["queued", "pending", "running"]: print(f"Current status: {status['status']}") time.sleep(2) # Wait 2 seconds - status = client.get_job_status(result.task_id) + status = client.get_workflow_execution( + "workflow-id", result.execution_id, include_output=True + ) if status["status"] == "completed": print("Workflow completed!") - print(f"Output: {status['output']}") - print(f"Duration: {status['metadata']['duration']}") + print(f"Output: {status['finalOutput']}") + print(f"Duration: {status['totalDurationMs']}") else: print(f"Workflow failed: {status['error']}") diff --git a/apps/docs/content/docs/es/sdks/typescript.mdx b/apps/docs/content/docs/es/sdks/typescript.mdx index 58c3578c219..40fb802636d 100644 --- a/apps/docs/content/docs/es/sdks/typescript.mdx +++ b/apps/docs/content/docs/es/sdks/typescript.mdx @@ -133,31 +133,37 @@ if (isReady) { **Devuelve:** `Promise` -##### getJobStatus() +##### getWorkflowExecution() -Obtener el estado de una ejecución de trabajo asíncrono. +Get the status and optional outputs of a workflow execution. ```typescript -const status = await client.getJobStatus('task-id-from-async-execution'); -console.log('Status:', status.status); // 'queued', 'processing', 'completed', 'failed' +const status = await client.getWorkflowExecution('workflow-id', 'execution-id', { + includeOutput: true +}); +console.log('Status:', status.status); // 'queued', 'running', 'completed', 'failed' if (status.status === 'completed') { - console.log('Output:', status.output); + console.log('Output:', status.finalOutput); } ``` -**Parámetros:** -- `taskId` (string): El ID de tarea devuelto por la ejecución asíncrona +**Parameters:** +- `workflowId` (string): The workflow ID +- `executionId` (string): The execution ID returned from async execution +- `options.includeOutput` (boolean, optional): Include the final output for completed executions +- `options.selectedOutputs` (string[], optional): Block output selectors to include -**Devuelve:** `Promise` +**Returns:** `Promise` -**Campos de respuesta:** -- `success` (boolean): Si la solicitud fue exitosa -- `taskId` (string): El ID de la tarea -- `status` (string): Uno de `'queued'`, `'processing'`, `'completed'`, `'failed'`, `'cancelled'` -- `metadata` (object): Contiene `startedAt`, `completedAt`, y `duration` -- `output` (any, opcional): La salida del flujo de trabajo (cuando se completa) -- `error` (any, opcional): Detalles del error (cuando falla) -- `estimatedDuration` (number, opcional): Duración estimada en milisegundos (cuando está procesando/en cola) +**Response fields:** +- `executionId` (string): The execution ID +- `workflowId` (string): The workflow ID +- `status` (string): One of `'queued'`, `'pending'`, `'running'`, `'paused'`, `'completed'`, `'failed'`, `'cancelled'` +- `startedAt` / `endedAt` (string): Execution timestamps +- `totalDurationMs` (number, nullable): Duration in milliseconds +- `finalOutput` (any, nullable): The workflow output when requested for a completed execution +- `blockOutputs` (object, nullable): Requested block outputs +- `error` (string, nullable): Failure details ##### executeWithRetry() @@ -286,12 +292,10 @@ interface WorkflowExecutionResult { ```typescript interface AsyncExecutionResult { success: boolean; - taskId: string; - status: 'queued'; - createdAt: string; - links: { - status: string; // e.g., "/api/jobs/{taskId}" - }; + executionId: string; + statusUrl: string; + message: string; + async: true; } ``` @@ -791,29 +795,32 @@ const client = new SimStudioClient({ async function executeAsync() { try { // Start async execution - const result = await client.executeWorkflow('workflow-id', { - input: { data: 'large dataset' }, + const result = await client.executeWorkflow('workflow-id', { data: 'large dataset' }, { async: true // Execute asynchronously }); // Check if result is an async execution - if ('taskId' in result) { - console.log('Task ID:', result.taskId); - console.log('Status endpoint:', result.links.status); + if ('async' in result && result.async) { + console.log('Execution ID:', result.executionId); + console.log('Status endpoint:', result.statusUrl); // Poll for completion - let status = await client.getJobStatus(result.taskId); + let status = await client.getWorkflowExecution('workflow-id', result.executionId, { + includeOutput: true + }); - while (status.status === 'queued' || status.status === 'processing') { + while (status.status === 'queued' || status.status === 'pending' || status.status === 'running') { console.log('Current status:', status.status); await new Promise(resolve => setTimeout(resolve, 2000)); // Wait 2 seconds - status = await client.getJobStatus(result.taskId); + status = await client.getWorkflowExecution('workflow-id', result.executionId, { + includeOutput: true + }); } if (status.status === 'completed') { console.log('Workflow completed!'); - console.log('Output:', status.output); - console.log('Duration:', status.metadata.duration); + console.log('Output:', status.finalOutput); + console.log('Duration:', status.totalDurationMs); } else { console.error('Workflow failed:', status.error); } diff --git a/apps/docs/content/docs/fr/api-reference/getting-started.mdx b/apps/docs/content/docs/fr/api-reference/getting-started.mdx index c8093e72c14..e593d678ff9 100644 --- a/apps/docs/content/docs/fr/api-reference/getting-started.mdx +++ b/apps/docs/content/docs/fr/api-reference/getting-started.mdx @@ -109,27 +109,27 @@ curl -X POST https://www.sim.ai/api/workflows/{workflowId}/execute \ -d '{"inputs": {}, "async": true}' ``` -This returns immediately with a `jobId` and `statusUrl`: +This returns immediately with an `executionId` and `statusUrl`: ```json { "success": true, - "jobId": "job_abc123", - "statusUrl": "https://www.sim.ai/api/jobs/job_abc123", - "message": "Workflow execution started", + "executionId": "c7a92e15-3f4b-4d8c-a1e6-9b0d5f2c8e74", + "statusUrl": "https://www.sim.ai/api/workflows/{workflowId}/executions/c7a92e15-3f4b-4d8c-a1e6-9b0d5f2c8e74", + "message": "Workflow execution queued", "async": true } ``` -Poll the [Get Job Status](/api-reference/execution/getJobStatus) endpoint until the status is `completed` or `failed`: +Poll the [Get Execution Status](/api-reference/execution/getWorkflowExecution) endpoint until the status is terminal: ```bash -curl https://www.sim.ai/api/jobs/{jobId} \ +curl https://www.sim.ai/api/workflows/{workflowId}/executions/{executionId}?includeOutput=true \ -H "X-API-Key: YOUR_API_KEY" ``` - Job status transitions follow: `queued` → `processing` → `completed` or `failed`. The `output` field is only present when status is `completed`. + Execution status transitions follow: `queued` → `running` → `completed`, `failed`, `cancelled`, or `paused`. The `finalOutput` field is populated for completed executions when `includeOutput=true`. ## Response Format diff --git a/apps/docs/content/docs/fr/api-reference/python.mdx b/apps/docs/content/docs/fr/api-reference/python.mdx index 268bc7657cf..797b759f276 100644 --- a/apps/docs/content/docs/fr/api-reference/python.mdx +++ b/apps/docs/content/docs/fr/api-reference/python.mdx @@ -112,30 +112,34 @@ if is_ready: **Retourne :** `bool` -##### get_job_status() +##### get_workflow_execution() -Obtenir le statut d'une exécution de tâche asynchrone. +Get the status and optional outputs of a workflow execution. ```python -status = client.get_job_status("task-id-from-async-execution") -print("Status:", status["status"]) # 'queued', 'processing', 'completed', 'failed' +status = client.get_workflow_execution("workflow-id", "execution-id", include_output=True) +print("Status:", status["status"]) # 'queued', 'running', 'completed', 'failed' if status["status"] == "completed": - print("Output:", status["output"]) + print("Output:", status["finalOutput"]) ``` -**Paramètres :** -- `task_id` (str) : L'identifiant de tâche retourné par l'exécution asynchrone +**Parameters:** +- `workflow_id` (str): The workflow ID +- `execution_id` (str): The execution ID returned from async execution +- `include_output` (bool, optional): Include the final output for completed executions +- `selected_outputs` (list[str], optional): Block output selectors to include -**Retourne :** `Dict[str, Any]` +**Returns:** `Dict[str, Any]` -**Champs de réponse :** -- `success` (bool) : Si la requête a réussi -- `taskId` (str) : L'identifiant de la tâche -- `status` (str) : L'un des états suivants : `'queued'`, `'processing'`, `'completed'`, `'failed'`, `'cancelled'` -- `metadata` (dict) : Contient `startedAt`, `completedAt`, et `duration` -- `output` (any, facultatif) : La sortie du workflow (une fois terminé) -- `error` (any, facultatif) : Détails de l'erreur (en cas d'échec) -- `estimatedDuration` (int, facultatif) : Durée estimée en millisecondes (lors du traitement/mise en file d'attente) +**Response fields:** +- `executionId` (str): The execution ID +- `workflowId` (str): The workflow ID +- `status` (str): One of `'queued'`, `'pending'`, `'running'`, `'paused'`, `'completed'`, `'failed'`, `'cancelled'` +- `startedAt` / `endedAt` (str): Execution timestamps +- `totalDurationMs` (int, optional): Duration in milliseconds +- `finalOutput` (any, optional): The workflow output when requested for a completed execution +- `blockOutputs` (dict, optional): Requested block outputs +- `error` (str, optional): Failure details ##### execute_with_retry() @@ -271,10 +275,10 @@ class WorkflowExecutionResult: @dataclass class AsyncExecutionResult: success: bool - task_id: str - status: str # 'queued' - created_at: str - links: Dict[str, str] # e.g., {"status": "/api/jobs/{taskId}"} + execution_id: str + status_url: str + message: str = "" + async_execution: bool = True ``` ### WorkflowStatus @@ -489,27 +493,31 @@ def execute_async(): # Start async execution result = client.execute_workflow( "workflow-id", - input_data={"data": "large dataset"}, + input={"data": "large dataset"}, async_execution=True # Execute asynchronously ) # Check if result is an async execution - if hasattr(result, 'task_id'): - print(f"Task ID: {result.task_id}") - print(f"Status endpoint: {result.links['status']}") + if hasattr(result, 'async_execution') and result.async_execution: + print(f"Execution ID: {result.execution_id}") + print(f"Status endpoint: {result.status_url}") # Poll for completion - status = client.get_job_status(result.task_id) + status = client.get_workflow_execution( + "workflow-id", result.execution_id, include_output=True + ) - while status["status"] in ["queued", "processing"]: + while status["status"] in ["queued", "pending", "running"]: print(f"Current status: {status['status']}") time.sleep(2) # Wait 2 seconds - status = client.get_job_status(result.task_id) + status = client.get_workflow_execution( + "workflow-id", result.execution_id, include_output=True + ) if status["status"] == "completed": print("Workflow completed!") - print(f"Output: {status['output']}") - print(f"Duration: {status['metadata']['duration']}") + print(f"Output: {status['finalOutput']}") + print(f"Duration: {status['totalDurationMs']}") else: print(f"Workflow failed: {status['error']}") diff --git a/apps/docs/content/docs/fr/api-reference/typescript.mdx b/apps/docs/content/docs/fr/api-reference/typescript.mdx index 0c6e98781af..4c23794f457 100644 --- a/apps/docs/content/docs/fr/api-reference/typescript.mdx +++ b/apps/docs/content/docs/fr/api-reference/typescript.mdx @@ -133,31 +133,37 @@ if (isReady) { **Retourne :** `Promise` -##### getJobStatus() +##### getWorkflowExecution() -Obtenir le statut d'une exécution de tâche asynchrone. +Get the status and optional outputs of a workflow execution. ```typescript -const status = await client.getJobStatus('task-id-from-async-execution'); -console.log('Status:', status.status); // 'queued', 'processing', 'completed', 'failed' +const status = await client.getWorkflowExecution('workflow-id', 'execution-id', { + includeOutput: true +}); +console.log('Status:', status.status); // 'queued', 'running', 'completed', 'failed' if (status.status === 'completed') { - console.log('Output:', status.output); + console.log('Output:', status.finalOutput); } ``` -**Paramètres :** -- `taskId` (string) : L'identifiant de tâche retourné par l'exécution asynchrone +**Parameters:** +- `workflowId` (string): The workflow ID +- `executionId` (string): The execution ID returned from async execution +- `options.includeOutput` (boolean, optional): Include the final output for completed executions +- `options.selectedOutputs` (string[], optional): Block output selectors to include -**Retourne :** `Promise` +**Returns:** `Promise` -**Champs de réponse :** -- `success` (boolean) : Indique si la requête a réussi -- `taskId` (string) : L'identifiant de la tâche -- `status` (string) : L'un des états suivants : `'queued'`, `'processing'`, `'completed'`, `'failed'`, `'cancelled'` -- `metadata` (object) : Contient `startedAt`, `completedAt` et `duration` -- `output` (any, facultatif) : La sortie du workflow (une fois terminé) -- `error` (any, facultatif) : Détails de l'erreur (en cas d'échec) -- `estimatedDuration` (number, facultatif) : Durée estimée en millisecondes (lorsqu'en traitement/en file d'attente) +**Response fields:** +- `executionId` (string): The execution ID +- `workflowId` (string): The workflow ID +- `status` (string): One of `'queued'`, `'pending'`, `'running'`, `'paused'`, `'completed'`, `'failed'`, `'cancelled'` +- `startedAt` / `endedAt` (string): Execution timestamps +- `totalDurationMs` (number, nullable): Duration in milliseconds +- `finalOutput` (any, nullable): The workflow output when requested for a completed execution +- `blockOutputs` (object, nullable): Requested block outputs +- `error` (string, nullable): Failure details ##### executeWithRetry() @@ -286,12 +292,10 @@ interface WorkflowExecutionResult { ```typescript interface AsyncExecutionResult { success: boolean; - taskId: string; - status: 'queued'; - createdAt: string; - links: { - status: string; // e.g., "/api/jobs/{taskId}" - }; + executionId: string; + statusUrl: string; + message: string; + async: true; } ``` @@ -791,29 +795,32 @@ const client = new SimStudioClient({ async function executeAsync() { try { // Start async execution - const result = await client.executeWorkflow('workflow-id', { - input: { data: 'large dataset' }, + const result = await client.executeWorkflow('workflow-id', { data: 'large dataset' }, { async: true // Execute asynchronously }); // Check if result is an async execution - if ('taskId' in result) { - console.log('Task ID:', result.taskId); - console.log('Status endpoint:', result.links.status); + if ('async' in result && result.async) { + console.log('Execution ID:', result.executionId); + console.log('Status endpoint:', result.statusUrl); // Poll for completion - let status = await client.getJobStatus(result.taskId); + let status = await client.getWorkflowExecution('workflow-id', result.executionId, { + includeOutput: true + }); - while (status.status === 'queued' || status.status === 'processing') { + while (status.status === 'queued' || status.status === 'pending' || status.status === 'running') { console.log('Current status:', status.status); await new Promise(resolve => setTimeout(resolve, 2000)); // Wait 2 seconds - status = await client.getJobStatus(result.taskId); + status = await client.getWorkflowExecution('workflow-id', result.executionId, { + includeOutput: true + }); } if (status.status === 'completed') { console.log('Workflow completed!'); - console.log('Output:', status.output); - console.log('Duration:', status.metadata.duration); + console.log('Output:', status.finalOutput); + console.log('Duration:', status.totalDurationMs); } else { console.error('Workflow failed:', status.error); } diff --git a/apps/docs/content/docs/fr/sdks/python.mdx b/apps/docs/content/docs/fr/sdks/python.mdx index 268bc7657cf..797b759f276 100644 --- a/apps/docs/content/docs/fr/sdks/python.mdx +++ b/apps/docs/content/docs/fr/sdks/python.mdx @@ -112,30 +112,34 @@ if is_ready: **Retourne :** `bool` -##### get_job_status() +##### get_workflow_execution() -Obtenir le statut d'une exécution de tâche asynchrone. +Get the status and optional outputs of a workflow execution. ```python -status = client.get_job_status("task-id-from-async-execution") -print("Status:", status["status"]) # 'queued', 'processing', 'completed', 'failed' +status = client.get_workflow_execution("workflow-id", "execution-id", include_output=True) +print("Status:", status["status"]) # 'queued', 'running', 'completed', 'failed' if status["status"] == "completed": - print("Output:", status["output"]) + print("Output:", status["finalOutput"]) ``` -**Paramètres :** -- `task_id` (str) : L'identifiant de tâche retourné par l'exécution asynchrone +**Parameters:** +- `workflow_id` (str): The workflow ID +- `execution_id` (str): The execution ID returned from async execution +- `include_output` (bool, optional): Include the final output for completed executions +- `selected_outputs` (list[str], optional): Block output selectors to include -**Retourne :** `Dict[str, Any]` +**Returns:** `Dict[str, Any]` -**Champs de réponse :** -- `success` (bool) : Si la requête a réussi -- `taskId` (str) : L'identifiant de la tâche -- `status` (str) : L'un des états suivants : `'queued'`, `'processing'`, `'completed'`, `'failed'`, `'cancelled'` -- `metadata` (dict) : Contient `startedAt`, `completedAt`, et `duration` -- `output` (any, facultatif) : La sortie du workflow (une fois terminé) -- `error` (any, facultatif) : Détails de l'erreur (en cas d'échec) -- `estimatedDuration` (int, facultatif) : Durée estimée en millisecondes (lors du traitement/mise en file d'attente) +**Response fields:** +- `executionId` (str): The execution ID +- `workflowId` (str): The workflow ID +- `status` (str): One of `'queued'`, `'pending'`, `'running'`, `'paused'`, `'completed'`, `'failed'`, `'cancelled'` +- `startedAt` / `endedAt` (str): Execution timestamps +- `totalDurationMs` (int, optional): Duration in milliseconds +- `finalOutput` (any, optional): The workflow output when requested for a completed execution +- `blockOutputs` (dict, optional): Requested block outputs +- `error` (str, optional): Failure details ##### execute_with_retry() @@ -271,10 +275,10 @@ class WorkflowExecutionResult: @dataclass class AsyncExecutionResult: success: bool - task_id: str - status: str # 'queued' - created_at: str - links: Dict[str, str] # e.g., {"status": "/api/jobs/{taskId}"} + execution_id: str + status_url: str + message: str = "" + async_execution: bool = True ``` ### WorkflowStatus @@ -489,27 +493,31 @@ def execute_async(): # Start async execution result = client.execute_workflow( "workflow-id", - input_data={"data": "large dataset"}, + input={"data": "large dataset"}, async_execution=True # Execute asynchronously ) # Check if result is an async execution - if hasattr(result, 'task_id'): - print(f"Task ID: {result.task_id}") - print(f"Status endpoint: {result.links['status']}") + if hasattr(result, 'async_execution') and result.async_execution: + print(f"Execution ID: {result.execution_id}") + print(f"Status endpoint: {result.status_url}") # Poll for completion - status = client.get_job_status(result.task_id) + status = client.get_workflow_execution( + "workflow-id", result.execution_id, include_output=True + ) - while status["status"] in ["queued", "processing"]: + while status["status"] in ["queued", "pending", "running"]: print(f"Current status: {status['status']}") time.sleep(2) # Wait 2 seconds - status = client.get_job_status(result.task_id) + status = client.get_workflow_execution( + "workflow-id", result.execution_id, include_output=True + ) if status["status"] == "completed": print("Workflow completed!") - print(f"Output: {status['output']}") - print(f"Duration: {status['metadata']['duration']}") + print(f"Output: {status['finalOutput']}") + print(f"Duration: {status['totalDurationMs']}") else: print(f"Workflow failed: {status['error']}") diff --git a/apps/docs/content/docs/fr/sdks/typescript.mdx b/apps/docs/content/docs/fr/sdks/typescript.mdx index 0c6e98781af..4c23794f457 100644 --- a/apps/docs/content/docs/fr/sdks/typescript.mdx +++ b/apps/docs/content/docs/fr/sdks/typescript.mdx @@ -133,31 +133,37 @@ if (isReady) { **Retourne :** `Promise` -##### getJobStatus() +##### getWorkflowExecution() -Obtenir le statut d'une exécution de tâche asynchrone. +Get the status and optional outputs of a workflow execution. ```typescript -const status = await client.getJobStatus('task-id-from-async-execution'); -console.log('Status:', status.status); // 'queued', 'processing', 'completed', 'failed' +const status = await client.getWorkflowExecution('workflow-id', 'execution-id', { + includeOutput: true +}); +console.log('Status:', status.status); // 'queued', 'running', 'completed', 'failed' if (status.status === 'completed') { - console.log('Output:', status.output); + console.log('Output:', status.finalOutput); } ``` -**Paramètres :** -- `taskId` (string) : L'identifiant de tâche retourné par l'exécution asynchrone +**Parameters:** +- `workflowId` (string): The workflow ID +- `executionId` (string): The execution ID returned from async execution +- `options.includeOutput` (boolean, optional): Include the final output for completed executions +- `options.selectedOutputs` (string[], optional): Block output selectors to include -**Retourne :** `Promise` +**Returns:** `Promise` -**Champs de réponse :** -- `success` (boolean) : Indique si la requête a réussi -- `taskId` (string) : L'identifiant de la tâche -- `status` (string) : L'un des états suivants : `'queued'`, `'processing'`, `'completed'`, `'failed'`, `'cancelled'` -- `metadata` (object) : Contient `startedAt`, `completedAt` et `duration` -- `output` (any, facultatif) : La sortie du workflow (une fois terminé) -- `error` (any, facultatif) : Détails de l'erreur (en cas d'échec) -- `estimatedDuration` (number, facultatif) : Durée estimée en millisecondes (lorsqu'en traitement/en file d'attente) +**Response fields:** +- `executionId` (string): The execution ID +- `workflowId` (string): The workflow ID +- `status` (string): One of `'queued'`, `'pending'`, `'running'`, `'paused'`, `'completed'`, `'failed'`, `'cancelled'` +- `startedAt` / `endedAt` (string): Execution timestamps +- `totalDurationMs` (number, nullable): Duration in milliseconds +- `finalOutput` (any, nullable): The workflow output when requested for a completed execution +- `blockOutputs` (object, nullable): Requested block outputs +- `error` (string, nullable): Failure details ##### executeWithRetry() @@ -286,12 +292,10 @@ interface WorkflowExecutionResult { ```typescript interface AsyncExecutionResult { success: boolean; - taskId: string; - status: 'queued'; - createdAt: string; - links: { - status: string; // e.g., "/api/jobs/{taskId}" - }; + executionId: string; + statusUrl: string; + message: string; + async: true; } ``` @@ -791,29 +795,32 @@ const client = new SimStudioClient({ async function executeAsync() { try { // Start async execution - const result = await client.executeWorkflow('workflow-id', { - input: { data: 'large dataset' }, + const result = await client.executeWorkflow('workflow-id', { data: 'large dataset' }, { async: true // Execute asynchronously }); // Check if result is an async execution - if ('taskId' in result) { - console.log('Task ID:', result.taskId); - console.log('Status endpoint:', result.links.status); + if ('async' in result && result.async) { + console.log('Execution ID:', result.executionId); + console.log('Status endpoint:', result.statusUrl); // Poll for completion - let status = await client.getJobStatus(result.taskId); + let status = await client.getWorkflowExecution('workflow-id', result.executionId, { + includeOutput: true + }); - while (status.status === 'queued' || status.status === 'processing') { + while (status.status === 'queued' || status.status === 'pending' || status.status === 'running') { console.log('Current status:', status.status); await new Promise(resolve => setTimeout(resolve, 2000)); // Wait 2 seconds - status = await client.getJobStatus(result.taskId); + status = await client.getWorkflowExecution('workflow-id', result.executionId, { + includeOutput: true + }); } if (status.status === 'completed') { console.log('Workflow completed!'); - console.log('Output:', status.output); - console.log('Duration:', status.metadata.duration); + console.log('Output:', status.finalOutput); + console.log('Duration:', status.totalDurationMs); } else { console.error('Workflow failed:', status.error); } diff --git a/apps/docs/content/docs/ja/api-reference/getting-started.mdx b/apps/docs/content/docs/ja/api-reference/getting-started.mdx index c8093e72c14..e593d678ff9 100644 --- a/apps/docs/content/docs/ja/api-reference/getting-started.mdx +++ b/apps/docs/content/docs/ja/api-reference/getting-started.mdx @@ -109,27 +109,27 @@ curl -X POST https://www.sim.ai/api/workflows/{workflowId}/execute \ -d '{"inputs": {}, "async": true}' ``` -This returns immediately with a `jobId` and `statusUrl`: +This returns immediately with an `executionId` and `statusUrl`: ```json { "success": true, - "jobId": "job_abc123", - "statusUrl": "https://www.sim.ai/api/jobs/job_abc123", - "message": "Workflow execution started", + "executionId": "c7a92e15-3f4b-4d8c-a1e6-9b0d5f2c8e74", + "statusUrl": "https://www.sim.ai/api/workflows/{workflowId}/executions/c7a92e15-3f4b-4d8c-a1e6-9b0d5f2c8e74", + "message": "Workflow execution queued", "async": true } ``` -Poll the [Get Job Status](/api-reference/execution/getJobStatus) endpoint until the status is `completed` or `failed`: +Poll the [Get Execution Status](/api-reference/execution/getWorkflowExecution) endpoint until the status is terminal: ```bash -curl https://www.sim.ai/api/jobs/{jobId} \ +curl https://www.sim.ai/api/workflows/{workflowId}/executions/{executionId}?includeOutput=true \ -H "X-API-Key: YOUR_API_KEY" ``` - Job status transitions follow: `queued` → `processing` → `completed` or `failed`. The `output` field is only present when status is `completed`. + Execution status transitions follow: `queued` → `running` → `completed`, `failed`, `cancelled`, or `paused`. The `finalOutput` field is populated for completed executions when `includeOutput=true`. ## Response Format diff --git a/apps/docs/content/docs/ja/api-reference/python.mdx b/apps/docs/content/docs/ja/api-reference/python.mdx index de4467f8a2a..14bb06c68b2 100644 --- a/apps/docs/content/docs/ja/api-reference/python.mdx +++ b/apps/docs/content/docs/ja/api-reference/python.mdx @@ -112,30 +112,34 @@ if is_ready: **戻り値:** `bool` -##### get_job_status() +##### get_workflow_execution() -非同期ジョブ実行のステータスを取得します。 +Get the status and optional outputs of a workflow execution. ```python -status = client.get_job_status("task-id-from-async-execution") -print("Status:", status["status"]) # 'queued', 'processing', 'completed', 'failed' +status = client.get_workflow_execution("workflow-id", "execution-id", include_output=True) +print("Status:", status["status"]) # 'queued', 'running', 'completed', 'failed' if status["status"] == "completed": - print("Output:", status["output"]) + print("Output:", status["finalOutput"]) ``` -**パラメータ:** -- `task_id` (str): 非同期実行から返されたタスクID +**Parameters:** +- `workflow_id` (str): The workflow ID +- `execution_id` (str): The execution ID returned from async execution +- `include_output` (bool, optional): Include the final output for completed executions +- `selected_outputs` (list[str], optional): Block output selectors to include -**戻り値:** `Dict[str, Any]` +**Returns:** `Dict[str, Any]` -**レスポンスフィールド:** -- `success` (bool): リクエストが成功したかどうか -- `taskId` (str): タスクID -- `status` (str): 次のいずれか: `'queued'`, `'processing'`, `'completed'`, `'failed'`, `'cancelled'` -- `metadata` (dict): `startedAt`, `completedAt`, `duration`を含む -- `output` (any, オプション): ワークフロー出力(完了時) -- `error` (any, オプション): エラー詳細(失敗時) -- `estimatedDuration` (int, オプション): 推定所要時間(ミリ秒)(処理中/キュー時) +**Response fields:** +- `executionId` (str): The execution ID +- `workflowId` (str): The workflow ID +- `status` (str): One of `'queued'`, `'pending'`, `'running'`, `'paused'`, `'completed'`, `'failed'`, `'cancelled'` +- `startedAt` / `endedAt` (str): Execution timestamps +- `totalDurationMs` (int, optional): Duration in milliseconds +- `finalOutput` (any, optional): The workflow output when requested for a completed execution +- `blockOutputs` (dict, optional): Requested block outputs +- `error` (str, optional): Failure details ##### execute_with_retry() @@ -271,10 +275,10 @@ class WorkflowExecutionResult: @dataclass class AsyncExecutionResult: success: bool - task_id: str - status: str # 'queued' - created_at: str - links: Dict[str, str] # e.g., {"status": "/api/jobs/{taskId}"} + execution_id: str + status_url: str + message: str = "" + async_execution: bool = True ``` ### WorkflowStatus @@ -489,27 +493,31 @@ def execute_async(): # Start async execution result = client.execute_workflow( "workflow-id", - input_data={"data": "large dataset"}, + input={"data": "large dataset"}, async_execution=True # Execute asynchronously ) # Check if result is an async execution - if hasattr(result, 'task_id'): - print(f"Task ID: {result.task_id}") - print(f"Status endpoint: {result.links['status']}") + if hasattr(result, 'async_execution') and result.async_execution: + print(f"Execution ID: {result.execution_id}") + print(f"Status endpoint: {result.status_url}") # Poll for completion - status = client.get_job_status(result.task_id) + status = client.get_workflow_execution( + "workflow-id", result.execution_id, include_output=True + ) - while status["status"] in ["queued", "processing"]: + while status["status"] in ["queued", "pending", "running"]: print(f"Current status: {status['status']}") time.sleep(2) # Wait 2 seconds - status = client.get_job_status(result.task_id) + status = client.get_workflow_execution( + "workflow-id", result.execution_id, include_output=True + ) if status["status"] == "completed": print("Workflow completed!") - print(f"Output: {status['output']}") - print(f"Duration: {status['metadata']['duration']}") + print(f"Output: {status['finalOutput']}") + print(f"Duration: {status['totalDurationMs']}") else: print(f"Workflow failed: {status['error']}") diff --git a/apps/docs/content/docs/ja/api-reference/typescript.mdx b/apps/docs/content/docs/ja/api-reference/typescript.mdx index a224c7663de..6fd184ecd0f 100644 --- a/apps/docs/content/docs/ja/api-reference/typescript.mdx +++ b/apps/docs/content/docs/ja/api-reference/typescript.mdx @@ -133,31 +133,37 @@ if (isReady) { **戻り値:** `Promise` -##### getJobStatus() +##### getWorkflowExecution() -非同期ジョブ実行のステータスを取得します。 +Get the status and optional outputs of a workflow execution. ```typescript -const status = await client.getJobStatus('task-id-from-async-execution'); -console.log('Status:', status.status); // 'queued', 'processing', 'completed', 'failed' +const status = await client.getWorkflowExecution('workflow-id', 'execution-id', { + includeOutput: true +}); +console.log('Status:', status.status); // 'queued', 'running', 'completed', 'failed' if (status.status === 'completed') { - console.log('Output:', status.output); + console.log('Output:', status.finalOutput); } ``` -**パラメータ:** -- `taskId` (string): 非同期実行から返されたタスクID +**Parameters:** +- `workflowId` (string): The workflow ID +- `executionId` (string): The execution ID returned from async execution +- `options.includeOutput` (boolean, optional): Include the final output for completed executions +- `options.selectedOutputs` (string[], optional): Block output selectors to include -**戻り値:** `Promise` +**Returns:** `Promise` -**レスポンスフィールド:** -- `success` (boolean): リクエストが成功したかどうか -- `taskId` (string): タスクID -- `status` (string): 次のいずれか `'queued'`, `'processing'`, `'completed'`, `'failed'`, `'cancelled'` -- `metadata` (object): `startedAt`, `completedAt`, および `duration` を含む -- `output` (any, オプション): ワークフロー出力(完了時) -- `error` (any, オプション): エラー詳細(失敗時) -- `estimatedDuration` (number, オプション): 推定所要時間(ミリ秒)(処理中/キュー時) +**Response fields:** +- `executionId` (string): The execution ID +- `workflowId` (string): The workflow ID +- `status` (string): One of `'queued'`, `'pending'`, `'running'`, `'paused'`, `'completed'`, `'failed'`, `'cancelled'` +- `startedAt` / `endedAt` (string): Execution timestamps +- `totalDurationMs` (number, nullable): Duration in milliseconds +- `finalOutput` (any, nullable): The workflow output when requested for a completed execution +- `blockOutputs` (object, nullable): Requested block outputs +- `error` (string, nullable): Failure details ##### executeWithRetry() @@ -286,12 +292,10 @@ interface WorkflowExecutionResult { ```typescript interface AsyncExecutionResult { success: boolean; - taskId: string; - status: 'queued'; - createdAt: string; - links: { - status: string; // e.g., "/api/jobs/{taskId}" - }; + executionId: string; + statusUrl: string; + message: string; + async: true; } ``` @@ -791,29 +795,32 @@ const client = new SimStudioClient({ async function executeAsync() { try { // Start async execution - const result = await client.executeWorkflow('workflow-id', { - input: { data: 'large dataset' }, + const result = await client.executeWorkflow('workflow-id', { data: 'large dataset' }, { async: true // Execute asynchronously }); // Check if result is an async execution - if ('taskId' in result) { - console.log('Task ID:', result.taskId); - console.log('Status endpoint:', result.links.status); + if ('async' in result && result.async) { + console.log('Execution ID:', result.executionId); + console.log('Status endpoint:', result.statusUrl); // Poll for completion - let status = await client.getJobStatus(result.taskId); + let status = await client.getWorkflowExecution('workflow-id', result.executionId, { + includeOutput: true + }); - while (status.status === 'queued' || status.status === 'processing') { + while (status.status === 'queued' || status.status === 'pending' || status.status === 'running') { console.log('Current status:', status.status); await new Promise(resolve => setTimeout(resolve, 2000)); // Wait 2 seconds - status = await client.getJobStatus(result.taskId); + status = await client.getWorkflowExecution('workflow-id', result.executionId, { + includeOutput: true + }); } if (status.status === 'completed') { console.log('Workflow completed!'); - console.log('Output:', status.output); - console.log('Duration:', status.metadata.duration); + console.log('Output:', status.finalOutput); + console.log('Duration:', status.totalDurationMs); } else { console.error('Workflow failed:', status.error); } diff --git a/apps/docs/content/docs/ja/sdks/python.mdx b/apps/docs/content/docs/ja/sdks/python.mdx index de4467f8a2a..14bb06c68b2 100644 --- a/apps/docs/content/docs/ja/sdks/python.mdx +++ b/apps/docs/content/docs/ja/sdks/python.mdx @@ -112,30 +112,34 @@ if is_ready: **戻り値:** `bool` -##### get_job_status() +##### get_workflow_execution() -非同期ジョブ実行のステータスを取得します。 +Get the status and optional outputs of a workflow execution. ```python -status = client.get_job_status("task-id-from-async-execution") -print("Status:", status["status"]) # 'queued', 'processing', 'completed', 'failed' +status = client.get_workflow_execution("workflow-id", "execution-id", include_output=True) +print("Status:", status["status"]) # 'queued', 'running', 'completed', 'failed' if status["status"] == "completed": - print("Output:", status["output"]) + print("Output:", status["finalOutput"]) ``` -**パラメータ:** -- `task_id` (str): 非同期実行から返されたタスクID +**Parameters:** +- `workflow_id` (str): The workflow ID +- `execution_id` (str): The execution ID returned from async execution +- `include_output` (bool, optional): Include the final output for completed executions +- `selected_outputs` (list[str], optional): Block output selectors to include -**戻り値:** `Dict[str, Any]` +**Returns:** `Dict[str, Any]` -**レスポンスフィールド:** -- `success` (bool): リクエストが成功したかどうか -- `taskId` (str): タスクID -- `status` (str): 次のいずれか: `'queued'`, `'processing'`, `'completed'`, `'failed'`, `'cancelled'` -- `metadata` (dict): `startedAt`, `completedAt`, `duration`を含む -- `output` (any, オプション): ワークフロー出力(完了時) -- `error` (any, オプション): エラー詳細(失敗時) -- `estimatedDuration` (int, オプション): 推定所要時間(ミリ秒)(処理中/キュー時) +**Response fields:** +- `executionId` (str): The execution ID +- `workflowId` (str): The workflow ID +- `status` (str): One of `'queued'`, `'pending'`, `'running'`, `'paused'`, `'completed'`, `'failed'`, `'cancelled'` +- `startedAt` / `endedAt` (str): Execution timestamps +- `totalDurationMs` (int, optional): Duration in milliseconds +- `finalOutput` (any, optional): The workflow output when requested for a completed execution +- `blockOutputs` (dict, optional): Requested block outputs +- `error` (str, optional): Failure details ##### execute_with_retry() @@ -271,10 +275,10 @@ class WorkflowExecutionResult: @dataclass class AsyncExecutionResult: success: bool - task_id: str - status: str # 'queued' - created_at: str - links: Dict[str, str] # e.g., {"status": "/api/jobs/{taskId}"} + execution_id: str + status_url: str + message: str = "" + async_execution: bool = True ``` ### WorkflowStatus @@ -489,27 +493,31 @@ def execute_async(): # Start async execution result = client.execute_workflow( "workflow-id", - input_data={"data": "large dataset"}, + input={"data": "large dataset"}, async_execution=True # Execute asynchronously ) # Check if result is an async execution - if hasattr(result, 'task_id'): - print(f"Task ID: {result.task_id}") - print(f"Status endpoint: {result.links['status']}") + if hasattr(result, 'async_execution') and result.async_execution: + print(f"Execution ID: {result.execution_id}") + print(f"Status endpoint: {result.status_url}") # Poll for completion - status = client.get_job_status(result.task_id) + status = client.get_workflow_execution( + "workflow-id", result.execution_id, include_output=True + ) - while status["status"] in ["queued", "processing"]: + while status["status"] in ["queued", "pending", "running"]: print(f"Current status: {status['status']}") time.sleep(2) # Wait 2 seconds - status = client.get_job_status(result.task_id) + status = client.get_workflow_execution( + "workflow-id", result.execution_id, include_output=True + ) if status["status"] == "completed": print("Workflow completed!") - print(f"Output: {status['output']}") - print(f"Duration: {status['metadata']['duration']}") + print(f"Output: {status['finalOutput']}") + print(f"Duration: {status['totalDurationMs']}") else: print(f"Workflow failed: {status['error']}") diff --git a/apps/docs/content/docs/ja/sdks/typescript.mdx b/apps/docs/content/docs/ja/sdks/typescript.mdx index a224c7663de..6fd184ecd0f 100644 --- a/apps/docs/content/docs/ja/sdks/typescript.mdx +++ b/apps/docs/content/docs/ja/sdks/typescript.mdx @@ -133,31 +133,37 @@ if (isReady) { **戻り値:** `Promise` -##### getJobStatus() +##### getWorkflowExecution() -非同期ジョブ実行のステータスを取得します。 +Get the status and optional outputs of a workflow execution. ```typescript -const status = await client.getJobStatus('task-id-from-async-execution'); -console.log('Status:', status.status); // 'queued', 'processing', 'completed', 'failed' +const status = await client.getWorkflowExecution('workflow-id', 'execution-id', { + includeOutput: true +}); +console.log('Status:', status.status); // 'queued', 'running', 'completed', 'failed' if (status.status === 'completed') { - console.log('Output:', status.output); + console.log('Output:', status.finalOutput); } ``` -**パラメータ:** -- `taskId` (string): 非同期実行から返されたタスクID +**Parameters:** +- `workflowId` (string): The workflow ID +- `executionId` (string): The execution ID returned from async execution +- `options.includeOutput` (boolean, optional): Include the final output for completed executions +- `options.selectedOutputs` (string[], optional): Block output selectors to include -**戻り値:** `Promise` +**Returns:** `Promise` -**レスポンスフィールド:** -- `success` (boolean): リクエストが成功したかどうか -- `taskId` (string): タスクID -- `status` (string): 次のいずれか `'queued'`, `'processing'`, `'completed'`, `'failed'`, `'cancelled'` -- `metadata` (object): `startedAt`, `completedAt`, および `duration` を含む -- `output` (any, オプション): ワークフロー出力(完了時) -- `error` (any, オプション): エラー詳細(失敗時) -- `estimatedDuration` (number, オプション): 推定所要時間(ミリ秒)(処理中/キュー時) +**Response fields:** +- `executionId` (string): The execution ID +- `workflowId` (string): The workflow ID +- `status` (string): One of `'queued'`, `'pending'`, `'running'`, `'paused'`, `'completed'`, `'failed'`, `'cancelled'` +- `startedAt` / `endedAt` (string): Execution timestamps +- `totalDurationMs` (number, nullable): Duration in milliseconds +- `finalOutput` (any, nullable): The workflow output when requested for a completed execution +- `blockOutputs` (object, nullable): Requested block outputs +- `error` (string, nullable): Failure details ##### executeWithRetry() @@ -286,12 +292,10 @@ interface WorkflowExecutionResult { ```typescript interface AsyncExecutionResult { success: boolean; - taskId: string; - status: 'queued'; - createdAt: string; - links: { - status: string; // e.g., "/api/jobs/{taskId}" - }; + executionId: string; + statusUrl: string; + message: string; + async: true; } ``` @@ -791,29 +795,32 @@ const client = new SimStudioClient({ async function executeAsync() { try { // Start async execution - const result = await client.executeWorkflow('workflow-id', { - input: { data: 'large dataset' }, + const result = await client.executeWorkflow('workflow-id', { data: 'large dataset' }, { async: true // Execute asynchronously }); // Check if result is an async execution - if ('taskId' in result) { - console.log('Task ID:', result.taskId); - console.log('Status endpoint:', result.links.status); + if ('async' in result && result.async) { + console.log('Execution ID:', result.executionId); + console.log('Status endpoint:', result.statusUrl); // Poll for completion - let status = await client.getJobStatus(result.taskId); + let status = await client.getWorkflowExecution('workflow-id', result.executionId, { + includeOutput: true + }); - while (status.status === 'queued' || status.status === 'processing') { + while (status.status === 'queued' || status.status === 'pending' || status.status === 'running') { console.log('Current status:', status.status); await new Promise(resolve => setTimeout(resolve, 2000)); // Wait 2 seconds - status = await client.getJobStatus(result.taskId); + status = await client.getWorkflowExecution('workflow-id', result.executionId, { + includeOutput: true + }); } if (status.status === 'completed') { console.log('Workflow completed!'); - console.log('Output:', status.output); - console.log('Duration:', status.metadata.duration); + console.log('Output:', status.finalOutput); + console.log('Duration:', status.totalDurationMs); } else { console.error('Workflow failed:', status.error); } diff --git a/apps/docs/content/docs/zh/api-reference/getting-started.mdx b/apps/docs/content/docs/zh/api-reference/getting-started.mdx index c8093e72c14..e593d678ff9 100644 --- a/apps/docs/content/docs/zh/api-reference/getting-started.mdx +++ b/apps/docs/content/docs/zh/api-reference/getting-started.mdx @@ -109,27 +109,27 @@ curl -X POST https://www.sim.ai/api/workflows/{workflowId}/execute \ -d '{"inputs": {}, "async": true}' ``` -This returns immediately with a `jobId` and `statusUrl`: +This returns immediately with an `executionId` and `statusUrl`: ```json { "success": true, - "jobId": "job_abc123", - "statusUrl": "https://www.sim.ai/api/jobs/job_abc123", - "message": "Workflow execution started", + "executionId": "c7a92e15-3f4b-4d8c-a1e6-9b0d5f2c8e74", + "statusUrl": "https://www.sim.ai/api/workflows/{workflowId}/executions/c7a92e15-3f4b-4d8c-a1e6-9b0d5f2c8e74", + "message": "Workflow execution queued", "async": true } ``` -Poll the [Get Job Status](/api-reference/execution/getJobStatus) endpoint until the status is `completed` or `failed`: +Poll the [Get Execution Status](/api-reference/execution/getWorkflowExecution) endpoint until the status is terminal: ```bash -curl https://www.sim.ai/api/jobs/{jobId} \ +curl https://www.sim.ai/api/workflows/{workflowId}/executions/{executionId}?includeOutput=true \ -H "X-API-Key: YOUR_API_KEY" ``` - Job status transitions follow: `queued` → `processing` → `completed` or `failed`. The `output` field is only present when status is `completed`. + Execution status transitions follow: `queued` → `running` → `completed`, `failed`, `cancelled`, or `paused`. The `finalOutput` field is populated for completed executions when `includeOutput=true`. ## Response Format diff --git a/apps/docs/content/docs/zh/api-reference/python.mdx b/apps/docs/content/docs/zh/api-reference/python.mdx index 608942d1baf..d73bc12083b 100644 --- a/apps/docs/content/docs/zh/api-reference/python.mdx +++ b/apps/docs/content/docs/zh/api-reference/python.mdx @@ -112,30 +112,34 @@ if is_ready: **返回值:** `bool` -##### get_job_status() +##### get_workflow_execution() -获取异步任务执行的状态。 +Get the status and optional outputs of a workflow execution. ```python -status = client.get_job_status("job-id-from-async-execution") -print("Status:", status["status"]) # 'queued', 'processing', 'completed', 'failed' +status = client.get_workflow_execution("workflow-id", "execution-id", include_output=True) +print("Status:", status["status"]) # 'queued', 'running', 'completed', 'failed' if status["status"] == "completed": - print("Output:", status["output"]) + print("Output:", status["finalOutput"]) ``` -**参数:** -- `job_id` (str): 异步执行返回的作业 ID +**Parameters:** +- `workflow_id` (str): The workflow ID +- `execution_id` (str): The execution ID returned from async execution +- `include_output` (bool, optional): Include the final output for completed executions +- `selected_outputs` (list[str], optional): Block output selectors to include -**返回值:** `Dict[str, Any]` +**Returns:** `Dict[str, Any]` -**响应字段:** -- `success` (bool): 请求是否成功 -- `taskId` (str): 作业 ID -- `status` (str): 可能的值包括 `'queued'`, `'processing'`, `'completed'`, `'failed'`, `'cancelled'` -- `metadata` (dict): 包含 `startedAt`, `completedAt` 和 `duration` -- `output` (any, optional): 工作流输出(完成时) -- `error` (any, optional): 错误详情(失败时) -- `estimatedDuration` (int, optional): 估计持续时间(以毫秒为单位,处理中/排队时) +**Response fields:** +- `executionId` (str): The execution ID +- `workflowId` (str): The workflow ID +- `status` (str): One of `'queued'`, `'pending'`, `'running'`, `'paused'`, `'completed'`, `'failed'`, `'cancelled'` +- `startedAt` / `endedAt` (str): Execution timestamps +- `totalDurationMs` (int, optional): Duration in milliseconds +- `finalOutput` (any, optional): The workflow output when requested for a completed execution +- `blockOutputs` (dict, optional): Requested block outputs +- `error` (str, optional): Failure details ##### execute_with_retry() @@ -271,9 +275,8 @@ class WorkflowExecutionResult: @dataclass class AsyncExecutionResult: success: bool - job_id: str + execution_id: str status_url: str - execution_id: Optional[str] = None message: str = "" async_execution: bool = True ``` @@ -490,27 +493,31 @@ def execute_async(): # Start async execution result = client.execute_workflow( "workflow-id", - input_data={"data": "large dataset"}, + input={"data": "large dataset"}, async_execution=True # Execute asynchronously ) # Check if result is an async execution - if hasattr(result, 'job_id'): - print(f"Job ID: {result.job_id}") + if hasattr(result, 'async_execution') and result.async_execution: + print(f"Execution ID: {result.execution_id}") print(f"Status endpoint: {result.status_url}") # Poll for completion - status = client.get_job_status(result.job_id) + status = client.get_workflow_execution( + "workflow-id", result.execution_id, include_output=True + ) - while status["status"] in ["queued", "processing"]: + while status["status"] in ["queued", "pending", "running"]: print(f"Current status: {status['status']}") time.sleep(2) # Wait 2 seconds - status = client.get_job_status(result.job_id) + status = client.get_workflow_execution( + "workflow-id", result.execution_id, include_output=True + ) if status["status"] == "completed": print("Workflow completed!") - print(f"Output: {status['output']}") - print(f"Duration: {status['metadata']['duration']}") + print(f"Output: {status['finalOutput']}") + print(f"Duration: {status['totalDurationMs']}") else: print(f"Workflow failed: {status['error']}") diff --git a/apps/docs/content/docs/zh/api-reference/typescript.mdx b/apps/docs/content/docs/zh/api-reference/typescript.mdx index fac3bdffb73..1b3eb86a6af 100644 --- a/apps/docs/content/docs/zh/api-reference/typescript.mdx +++ b/apps/docs/content/docs/zh/api-reference/typescript.mdx @@ -133,31 +133,37 @@ if (isReady) { **返回值:** `Promise` -##### getJobStatus() +##### getWorkflowExecution() -获取异步任务执行的状态。 +Get the status and optional outputs of a workflow execution. ```typescript -const status = await client.getJobStatus('job-id-from-async-execution'); -console.log('Status:', status.status); // 'queued', 'processing', 'completed', 'failed' +const status = await client.getWorkflowExecution('workflow-id', 'execution-id', { + includeOutput: true +}); +console.log('Status:', status.status); // 'queued', 'running', 'completed', 'failed' if (status.status === 'completed') { - console.log('Output:', status.output); + console.log('Output:', status.finalOutput); } ``` -**参数:** -- `jobId`(字符串):异步执行返回的作业 ID +**Parameters:** +- `workflowId` (string): The workflow ID +- `executionId` (string): The execution ID returned from async execution +- `options.includeOutput` (boolean, optional): Include the final output for completed executions +- `options.selectedOutputs` (string[], optional): Block output selectors to include -**返回值:** `Promise` +**Returns:** `Promise` -**响应字段:** -- `success`(布尔值):请求是否成功 -- `taskId`(字符串):作业 ID -- `status`(字符串):以下之一 `'queued'`、`'processing'`、`'completed'`、`'failed'`、`'cancelled'` -- `metadata`(对象):包含 `startedAt`、`completedAt` 和 `duration` -- `output`(任意类型,可选):工作流输出(完成时) -- `error`(任意类型,可选):错误详情(失败时) -- `estimatedDuration`(数字,可选):估计持续时间(以毫秒为单位,处理中/排队时) +**Response fields:** +- `executionId` (string): The execution ID +- `workflowId` (string): The workflow ID +- `status` (string): One of `'queued'`, `'pending'`, `'running'`, `'paused'`, `'completed'`, `'failed'`, `'cancelled'` +- `startedAt` / `endedAt` (string): Execution timestamps +- `totalDurationMs` (number, nullable): Duration in milliseconds +- `finalOutput` (any, nullable): The workflow output when requested for a completed execution +- `blockOutputs` (object, nullable): Requested block outputs +- `error` (string, nullable): Failure details ##### executeWithRetry() @@ -286,9 +292,8 @@ interface WorkflowExecutionResult { ```typescript interface AsyncExecutionResult { success: boolean; - jobId: string; + executionId: string; statusUrl: string; - executionId?: string; message: string; async: true; } @@ -790,29 +795,32 @@ const client = new SimStudioClient({ async function executeAsync() { try { // Start async execution - const result = await client.executeWorkflow('workflow-id', { - input: { data: 'large dataset' }, + const result = await client.executeWorkflow('workflow-id', { data: 'large dataset' }, { async: true // Execute asynchronously }); // Check if result is an async execution - if ('jobId' in result) { - console.log('Job ID:', result.jobId); + if ('async' in result && result.async) { + console.log('Execution ID:', result.executionId); console.log('Status endpoint:', result.statusUrl); // Poll for completion - let status = await client.getJobStatus(result.jobId); + let status = await client.getWorkflowExecution('workflow-id', result.executionId, { + includeOutput: true + }); - while (status.status === 'queued' || status.status === 'processing') { + while (status.status === 'queued' || status.status === 'pending' || status.status === 'running') { console.log('Current status:', status.status); await new Promise(resolve => setTimeout(resolve, 2000)); // Wait 2 seconds - status = await client.getJobStatus(result.jobId); + status = await client.getWorkflowExecution('workflow-id', result.executionId, { + includeOutput: true + }); } if (status.status === 'completed') { console.log('Workflow completed!'); - console.log('Output:', status.output); - console.log('Duration:', status.metadata.duration); + console.log('Output:', status.finalOutput); + console.log('Duration:', status.totalDurationMs); } else { console.error('Workflow failed:', status.error); } diff --git a/apps/docs/content/docs/zh/sdks/python.mdx b/apps/docs/content/docs/zh/sdks/python.mdx index c44973c8660..d73bc12083b 100644 --- a/apps/docs/content/docs/zh/sdks/python.mdx +++ b/apps/docs/content/docs/zh/sdks/python.mdx @@ -112,30 +112,34 @@ if is_ready: **返回值:** `bool` -##### get_job_status() +##### get_workflow_execution() -获取异步任务执行的状态。 +Get the status and optional outputs of a workflow execution. ```python -status = client.get_job_status("task-id-from-async-execution") -print("Status:", status["status"]) # 'queued', 'processing', 'completed', 'failed' +status = client.get_workflow_execution("workflow-id", "execution-id", include_output=True) +print("Status:", status["status"]) # 'queued', 'running', 'completed', 'failed' if status["status"] == "completed": - print("Output:", status["output"]) + print("Output:", status["finalOutput"]) ``` -**参数:** -- `task_id` (str): 异步执行返回的任务 ID +**Parameters:** +- `workflow_id` (str): The workflow ID +- `execution_id` (str): The execution ID returned from async execution +- `include_output` (bool, optional): Include the final output for completed executions +- `selected_outputs` (list[str], optional): Block output selectors to include -**返回值:** `Dict[str, Any]` +**Returns:** `Dict[str, Any]` -**响应字段:** -- `success` (bool): 请求是否成功 -- `taskId` (str): 任务 ID -- `status` (str): 可能的值包括 `'queued'`, `'processing'`, `'completed'`, `'failed'`, `'cancelled'` -- `metadata` (dict): 包含 `startedAt`, `completedAt` 和 `duration` -- `output` (any, optional): 工作流输出(完成时) -- `error` (any, optional): 错误详情(失败时) -- `estimatedDuration` (int, optional): 估计持续时间(以毫秒为单位,处理中/排队时) +**Response fields:** +- `executionId` (str): The execution ID +- `workflowId` (str): The workflow ID +- `status` (str): One of `'queued'`, `'pending'`, `'running'`, `'paused'`, `'completed'`, `'failed'`, `'cancelled'` +- `startedAt` / `endedAt` (str): Execution timestamps +- `totalDurationMs` (int, optional): Duration in milliseconds +- `finalOutput` (any, optional): The workflow output when requested for a completed execution +- `blockOutputs` (dict, optional): Requested block outputs +- `error` (str, optional): Failure details ##### execute_with_retry() @@ -271,10 +275,10 @@ class WorkflowExecutionResult: @dataclass class AsyncExecutionResult: success: bool - task_id: str - status: str # 'queued' - created_at: str - links: Dict[str, str] # e.g., {"status": "/api/jobs/{taskId}"} + execution_id: str + status_url: str + message: str = "" + async_execution: bool = True ``` ### WorkflowStatus @@ -489,27 +493,31 @@ def execute_async(): # Start async execution result = client.execute_workflow( "workflow-id", - input_data={"data": "large dataset"}, + input={"data": "large dataset"}, async_execution=True # Execute asynchronously ) # Check if result is an async execution - if hasattr(result, 'task_id'): - print(f"Task ID: {result.task_id}") - print(f"Status endpoint: {result.links['status']}") + if hasattr(result, 'async_execution') and result.async_execution: + print(f"Execution ID: {result.execution_id}") + print(f"Status endpoint: {result.status_url}") # Poll for completion - status = client.get_job_status(result.task_id) + status = client.get_workflow_execution( + "workflow-id", result.execution_id, include_output=True + ) - while status["status"] in ["queued", "processing"]: + while status["status"] in ["queued", "pending", "running"]: print(f"Current status: {status['status']}") time.sleep(2) # Wait 2 seconds - status = client.get_job_status(result.task_id) + status = client.get_workflow_execution( + "workflow-id", result.execution_id, include_output=True + ) if status["status"] == "completed": print("Workflow completed!") - print(f"Output: {status['output']}") - print(f"Duration: {status['metadata']['duration']}") + print(f"Output: {status['finalOutput']}") + print(f"Duration: {status['totalDurationMs']}") else: print(f"Workflow failed: {status['error']}") diff --git a/apps/docs/content/docs/zh/sdks/typescript.mdx b/apps/docs/content/docs/zh/sdks/typescript.mdx index 0f038db92dd..1b3eb86a6af 100644 --- a/apps/docs/content/docs/zh/sdks/typescript.mdx +++ b/apps/docs/content/docs/zh/sdks/typescript.mdx @@ -133,31 +133,37 @@ if (isReady) { **返回值:** `Promise` -##### getJobStatus() +##### getWorkflowExecution() -获取异步任务执行的状态。 +Get the status and optional outputs of a workflow execution. ```typescript -const status = await client.getJobStatus('task-id-from-async-execution'); -console.log('Status:', status.status); // 'queued', 'processing', 'completed', 'failed' +const status = await client.getWorkflowExecution('workflow-id', 'execution-id', { + includeOutput: true +}); +console.log('Status:', status.status); // 'queued', 'running', 'completed', 'failed' if (status.status === 'completed') { - console.log('Output:', status.output); + console.log('Output:', status.finalOutput); } ``` -**参数:** -- `taskId`(字符串):异步执行返回的任务 ID +**Parameters:** +- `workflowId` (string): The workflow ID +- `executionId` (string): The execution ID returned from async execution +- `options.includeOutput` (boolean, optional): Include the final output for completed executions +- `options.selectedOutputs` (string[], optional): Block output selectors to include -**返回值:** `Promise` +**Returns:** `Promise` -**响应字段:** -- `success`(布尔值):请求是否成功 -- `taskId`(字符串):任务 ID -- `status`(字符串):以下之一 `'queued'`、`'processing'`、`'completed'`、`'failed'`、`'cancelled'` -- `metadata`(对象):包含 `startedAt`、`completedAt` 和 `duration` -- `output`(任意类型,可选):工作流输出(完成时) -- `error`(任意类型,可选):错误详情(失败时) -- `estimatedDuration`(数字,可选):估计持续时间(以毫秒为单位,处理中/排队时) +**Response fields:** +- `executionId` (string): The execution ID +- `workflowId` (string): The workflow ID +- `status` (string): One of `'queued'`, `'pending'`, `'running'`, `'paused'`, `'completed'`, `'failed'`, `'cancelled'` +- `startedAt` / `endedAt` (string): Execution timestamps +- `totalDurationMs` (number, nullable): Duration in milliseconds +- `finalOutput` (any, nullable): The workflow output when requested for a completed execution +- `blockOutputs` (object, nullable): Requested block outputs +- `error` (string, nullable): Failure details ##### executeWithRetry() @@ -286,12 +292,10 @@ interface WorkflowExecutionResult { ```typescript interface AsyncExecutionResult { success: boolean; - taskId: string; - status: 'queued'; - createdAt: string; - links: { - status: string; // e.g., "/api/jobs/{taskId}" - }; + executionId: string; + statusUrl: string; + message: string; + async: true; } ``` @@ -791,29 +795,32 @@ const client = new SimStudioClient({ async function executeAsync() { try { // Start async execution - const result = await client.executeWorkflow('workflow-id', { - input: { data: 'large dataset' }, + const result = await client.executeWorkflow('workflow-id', { data: 'large dataset' }, { async: true // Execute asynchronously }); // Check if result is an async execution - if ('taskId' in result) { - console.log('Task ID:', result.taskId); - console.log('Status endpoint:', result.links.status); + if ('async' in result && result.async) { + console.log('Execution ID:', result.executionId); + console.log('Status endpoint:', result.statusUrl); // Poll for completion - let status = await client.getJobStatus(result.taskId); + let status = await client.getWorkflowExecution('workflow-id', result.executionId, { + includeOutput: true + }); - while (status.status === 'queued' || status.status === 'processing') { + while (status.status === 'queued' || status.status === 'pending' || status.status === 'running') { console.log('Current status:', status.status); await new Promise(resolve => setTimeout(resolve, 2000)); // Wait 2 seconds - status = await client.getJobStatus(result.taskId); + status = await client.getWorkflowExecution('workflow-id', result.executionId, { + includeOutput: true + }); } if (status.status === 'completed') { console.log('Workflow completed!'); - console.log('Output:', status.output); - console.log('Duration:', status.metadata.duration); + console.log('Output:', status.finalOutput); + console.log('Duration:', status.totalDurationMs); } else { console.error('Workflow failed:', status.error); } diff --git a/apps/docs/openapi-core.json b/apps/docs/openapi-core.json index 6ed28693188..4df6ecce4ea 100644 --- a/apps/docs/openapi-core.json +++ b/apps/docs/openapi-core.json @@ -153,7 +153,7 @@ "get": { "operationId": "getWorkflowExecution", "summary": "Get Execution Status", - "description": "Get the current status of a workflow execution. Returns the run's lifecycle state (`running`, `paused`, `completed`, `failed`, etc.), timing, error, and optionally per-block outputs. Designed for polling — works for any execution, including ones that pause and resume.", + "description": "Get the current status of a workflow execution. Returns `queued` immediately after async dispatch, then the run's durable lifecycle state (`running`, `paused`, `completed`, `failed`, etc.), timing, error, and optionally per-block outputs. This is the canonical polling resource for synchronous executions, asynchronous executions, and resume attempts.", "tags": ["Execution"], "x-codeSamples": [ { @@ -371,66 +371,6 @@ } } }, - "/api/jobs/{jobId}": { - "get": { - "operationId": "getJobStatus", - "summary": "Get Job Status", - "description": "Poll the status of an asynchronous workflow execution. Use the jobId returned from the Execute Workflow endpoint when the execution is queued asynchronously.", - "tags": ["Execution"], - "x-codeSamples": [ - { - "id": "curl", - "label": "cURL", - "lang": "bash", - "source": "curl -X GET \\\n \"https://www.sim.ai/api/jobs/{jobId}\" \\\n -H \"X-API-Key: YOUR_API_KEY\"" - } - ], - "parameters": [ - { - "name": "jobId", - "in": "path", - "required": true, - "description": "The job identifier returned in the async execution response.", - "schema": { - "type": "string", - "example": "job_4a3b2c1d0e" - } - } - ], - "responses": { - "200": { - "description": "Current status of the job. When completed, includes the execution output.", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/JobStatus" - }, - "example": { - "success": true, - "taskId": "job_abc123", - "status": "completed", - "output": { - "content": "Done" - }, - "metadata": { - "startTime": "2026-01-15T10:30:00Z" - } - } - } - } - }, - "401": { - "$ref": "#/components/responses/Unauthorized" - }, - "403": { - "$ref": "#/components/responses/Forbidden" - }, - "404": { - "$ref": "#/components/responses/NotFound" - } - } - } - }, "/api/workflows/{id}/paused": { "get": { "operationId": "listPausedExecutions", @@ -914,10 +854,9 @@ "example": { "success": true, "async": true, - "jobId": "job_4a3b2c1d0e", "executionId": "f0b3d8c2-7e5a-4b9d-8c1f-6a4e2d0b9c58", "message": "Resume execution queued", - "statusUrl": "https://www.sim.ai/api/jobs/job_4a3b2c1d0e" + "statusUrl": "https://www.sim.ai/api/workflows/3b1f7c92-8d4e-4a6b-9c0d-5e2f8a714b36/executions/f0b3d8c2-7e5a-4b9d-8c1f-6a4e2d0b9c58" } } } @@ -1394,6 +1333,7 @@ "AsyncExecutionResult": { "type": "object", "description": "Response returned when a workflow execution is queued for asynchronous processing.", + "required": ["success", "async", "executionId", "message", "statusUrl"], "properties": { "success": { "type": "boolean", @@ -1405,11 +1345,6 @@ "description": "Always true for async executions. Use this to distinguish from synchronous responses.", "example": true }, - "jobId": { - "type": "string", - "description": "Internal job queue identifier for tracking the execution.", - "example": "job_4a3b2c1d0e" - }, "executionId": { "type": "string", "description": "Unique execution identifier. Use this to query execution status or cancel.", @@ -1424,69 +1359,7 @@ "type": "string", "format": "uri", "description": "URL to poll for execution status and results. Returns the full execution result once complete.", - "example": "https://www.sim.ai/api/jobs/job_4a3b2c1d0e" - } - } - }, - "JobStatus": { - "type": "object", - "description": "Status of an asynchronous job.", - "properties": { - "success": { - "type": "boolean", - "description": "Whether the request was successful.", - "example": true - }, - "taskId": { - "type": "string", - "description": "The unique identifier of the job.", - "example": "job_4a3b2c1d0e" - }, - "status": { - "type": "string", - "enum": ["queued", "processing", "completed", "failed"], - "description": "Current status of the job.", - "example": "completed" - }, - "metadata": { - "type": "object", - "description": "Timing metadata for the job.", - "properties": { - "startedAt": { - "type": "string", - "format": "date-time", - "description": "ISO 8601 timestamp when the job started processing.", - "example": "2025-06-20T14:15:22Z" - }, - "completedAt": { - "type": "string", - "format": "date-time", - "description": "ISO 8601 timestamp when the job completed. Present only when status is completed or failed.", - "example": "2025-06-20T14:15:23Z" - }, - "duration": { - "type": "integer", - "description": "Duration of the job in milliseconds. Present only when status is completed or failed.", - "example": 1250 - } - } - }, - "output": { - "description": "The workflow execution output. Present only when status is completed.", - "type": "object", - "example": { - "result": "Hello, world!" - } - }, - "error": { - "description": "Error details. Present only when status is failed.", - "type": "string", - "example": null - }, - "estimatedDuration": { - "type": "integer", - "description": "Estimated duration in milliseconds. Present only when status is queued or processing.", - "example": 2000 + "example": "https://www.sim.ai/api/workflows/3b1f7c92-8d4e-4a6b-9c0d-5e2f8a714b36/executions/e4f8d2b6-9a1c-4e3d-8b7f-5c0a2d9e6f13" } } }, @@ -1506,8 +1379,8 @@ }, "status": { "type": "string", - "enum": ["pending", "running", "paused", "completed", "failed", "cancelled"], - "description": "Current normalized lifecycle status. `paused` is set when a row exists in pausedExecutions with status `paused` or `partially_resumed`; otherwise the workflowExecutionLogs row's status field is used.", + "enum": ["queued", "pending", "running", "paused", "completed", "failed", "cancelled"], + "description": "Current normalized lifecycle status. `queued` is projected from the async queue before the durable execution log exists; `paused` is set when a row exists in pausedExecutions with status `paused` or `partially_resumed`; otherwise the workflowExecutionLogs row's status field is used.", "example": "completed" }, "trigger": { diff --git a/apps/docs/openapi.json b/apps/docs/openapi.json index b2e8ca4c523..888d3f5e469 100644 --- a/apps/docs/openapi.json +++ b/apps/docs/openapi.json @@ -173,7 +173,7 @@ "get": { "operationId": "getWorkflowExecution", "summary": "Get Execution Status", - "description": "Get the current status of a workflow execution. Returns the run's lifecycle state (`running`, `paused`, `completed`, `failed`, etc.), timing, error, and optionally per-block outputs. Designed for polling \u2014 works for any execution, including ones that pause and resume.", + "description": "Get the current status of a workflow execution. Returns `queued` immediately after async dispatch, then the run's durable lifecycle state (`running`, `paused`, `completed`, `failed`, etc.), timing, error, and optionally per-block outputs. This is the canonical polling resource for synchronous executions, asynchronous executions, and resume attempts.", "tags": ["Workflows"], "x-codeSamples": [ { @@ -874,10 +874,9 @@ "example": { "success": true, "async": true, - "jobId": "job_4a3b2c1d0e", "executionId": "f0b3d8c2-7e5a-4b9d-8c1f-6a4e2d0b9c58", "message": "Resume execution queued", - "statusUrl": "https://www.sim.ai/api/jobs/job_4a3b2c1d0e" + "statusUrl": "https://www.sim.ai/api/workflows/3b1f7c92-8d4e-4a6b-9c0d-5e2f8a714b36/executions/f0b3d8c2-7e5a-4b9d-8c1f-6a4e2d0b9c58" } } } @@ -1726,66 +1725,6 @@ } } }, - "/api/jobs/{jobId}": { - "get": { - "operationId": "getJobStatus", - "summary": "Get Job Status", - "description": "Poll the status of an asynchronous workflow execution. Use the jobId returned from the Execute Workflow endpoint when the execution is queued asynchronously.", - "tags": ["Workflows"], - "x-codeSamples": [ - { - "id": "curl", - "label": "cURL", - "lang": "bash", - "source": "curl -X GET \\\n \"https://www.sim.ai/api/jobs/{jobId}\" \\\n -H \"X-API-Key: YOUR_API_KEY\"" - } - ], - "parameters": [ - { - "name": "jobId", - "in": "path", - "required": true, - "description": "The job identifier returned in the async execution response.", - "schema": { - "type": "string", - "example": "job_4a3b2c1d0e" - } - } - ], - "responses": { - "200": { - "description": "Current status of the job. When completed, includes the execution output.", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/JobStatus" - }, - "example": { - "success": true, - "taskId": "job_abc123", - "status": "completed", - "output": { - "content": "Done" - }, - "metadata": { - "startTime": "2026-01-15T10:30:00Z" - } - } - } - } - }, - "401": { - "$ref": "#/components/responses/Unauthorized" - }, - "403": { - "$ref": "#/components/responses/Forbidden" - }, - "404": { - "$ref": "#/components/responses/NotFound" - } - } - } - }, "/api/v1/logs": { "get": { "operationId": "queryLogs", @@ -6611,6 +6550,7 @@ "AsyncExecutionResult": { "type": "object", "description": "Response returned when a workflow execution is queued for asynchronous processing.", + "required": ["success", "async", "executionId", "message", "statusUrl"], "properties": { "success": { "type": "boolean", @@ -6622,11 +6562,6 @@ "description": "Always true for async executions. Use this to distinguish from synchronous responses.", "example": true }, - "jobId": { - "type": "string", - "description": "Internal job queue identifier for tracking the execution.", - "example": "job_4a3b2c1d0e" - }, "executionId": { "type": "string", "description": "Unique execution identifier. Use this to query execution status or cancel.", @@ -6641,7 +6576,7 @@ "type": "string", "format": "uri", "description": "URL to poll for execution status and results. Returns the full execution result once complete.", - "example": "https://www.sim.ai/api/jobs/job_4a3b2c1d0e" + "example": "https://www.sim.ai/api/workflows/3b1f7c92-8d4e-4a6b-9c0d-5e2f8a714b36/executions/e4f8d2b6-9a1c-4e3d-8b7f-5c0a2d9e6f13" } } }, @@ -6942,68 +6877,6 @@ } } }, - "JobStatus": { - "type": "object", - "description": "Status of an asynchronous job.", - "properties": { - "success": { - "type": "boolean", - "description": "Whether the request was successful.", - "example": true - }, - "taskId": { - "type": "string", - "description": "The unique identifier of the job.", - "example": "job_4a3b2c1d0e" - }, - "status": { - "type": "string", - "enum": ["queued", "processing", "completed", "failed"], - "description": "Current status of the job.", - "example": "completed" - }, - "metadata": { - "type": "object", - "description": "Timing metadata for the job.", - "properties": { - "startedAt": { - "type": "string", - "format": "date-time", - "description": "ISO 8601 timestamp when the job started processing.", - "example": "2025-06-20T14:15:22Z" - }, - "completedAt": { - "type": "string", - "format": "date-time", - "description": "ISO 8601 timestamp when the job completed. Present only when status is completed or failed.", - "example": "2025-06-20T14:15:23Z" - }, - "duration": { - "type": "integer", - "description": "Duration of the job in milliseconds. Present only when status is completed or failed.", - "example": 1250 - } - } - }, - "output": { - "description": "The workflow execution output. Present only when status is completed.", - "type": "object", - "example": { - "result": "Hello, world!" - } - }, - "error": { - "description": "Error details. Present only when status is failed.", - "type": "string", - "example": null - }, - "estimatedDuration": { - "type": "integer", - "description": "Estimated duration in milliseconds. Present only when status is queued or processing.", - "example": 2000 - } - } - }, "WorkflowExecutionStatus": { "type": "object", "description": "Current status of a workflow execution.", @@ -7020,8 +6893,8 @@ }, "status": { "type": "string", - "enum": ["pending", "running", "paused", "completed", "failed", "cancelled"], - "description": "Current normalized lifecycle status. `paused` is set when a row exists in pausedExecutions with status `paused` or `partially_resumed`; otherwise the workflowExecutionLogs row's status field is used.", + "enum": ["queued", "pending", "running", "paused", "completed", "failed", "cancelled"], + "description": "Current normalized lifecycle status. `queued` is projected from the async queue before the durable execution log exists; `paused` is set when a row exists in pausedExecutions with status `paused` or `partially_resumed`; otherwise the workflowExecutionLogs row's status field is used.", "example": "completed" }, "trigger": { diff --git a/apps/sim/app/api/jobs/[jobId]/route.test.ts b/apps/sim/app/api/jobs/[jobId]/route.test.ts deleted file mode 100644 index 5fc7de85435..00000000000 --- a/apps/sim/app/api/jobs/[jobId]/route.test.ts +++ /dev/null @@ -1,102 +0,0 @@ -/** - * @vitest-environment node - */ -import { - hybridAuthMockFns, - workflowAuthzMockFns, - workflowsUtilsMock, - workflowsUtilsMockFns, -} from '@sim/testing' -import { NextRequest } from 'next/server' -import { beforeEach, describe, expect, it, vi } from 'vitest' - -const { mockGetJobQueue, mockGetJob } = vi.hoisted(() => ({ - mockGetJobQueue: vi.fn(), - mockGetJob: vi.fn(), -})) - -vi.mock('@/lib/core/async-jobs', () => ({ - getJobQueue: mockGetJobQueue, -})) - -const mockAuthorizeWorkflow = workflowAuthzMockFns.mockAuthorizeWorkflowByWorkspacePermission - -vi.mock('@/lib/workflows/utils', () => workflowsUtilsMock) - -import { GET } from './route' - -function createMockRequest(): NextRequest { - return new NextRequest(new URL('http://localhost:3000/api/jobs/test')) -} - -describe('GET /api/jobs/[jobId]', () => { - beforeEach(() => { - vi.clearAllMocks() - - hybridAuthMockFns.mockCheckHybridAuth.mockResolvedValue({ - success: true, - userId: 'user-1', - apiKeyType: undefined, - workspaceId: undefined, - }) - - mockAuthorizeWorkflow.mockResolvedValue({ allowed: true, status: 200 }) - workflowsUtilsMockFns.mockGetWorkflowById.mockResolvedValue({ - id: 'workflow-1', - workspaceId: 'workspace-1', - }) - - mockGetJobQueue.mockResolvedValue({ - getJob: mockGetJob, - }) - }) - - it('returns job status with metadata', async () => { - mockGetJob.mockResolvedValue({ - id: 'job-1', - status: 'pending', - metadata: { - workflowId: 'workflow-1', - }, - }) - - const response = await GET(createMockRequest(), { - params: Promise.resolve({ jobId: 'job-1' }), - }) - const body = await response.json() - - expect(response.status).toBe(200) - expect(body.status).toBe('pending') - expect(body.metadata.workflowId).toBe('workflow-1') - }) - - it('returns completed output from job', async () => { - mockGetJob.mockResolvedValue({ - id: 'job-2', - status: 'completed', - metadata: { - workflowId: 'workflow-1', - }, - output: { success: true }, - }) - - const response = await GET(createMockRequest(), { - params: Promise.resolve({ jobId: 'job-2' }), - }) - const body = await response.json() - - expect(response.status).toBe(200) - expect(body.status).toBe('completed') - expect(body.output).toEqual({ success: true }) - }) - - it('returns 404 when job does not exist', async () => { - mockGetJob.mockResolvedValue(null) - - const response = await GET(createMockRequest(), { - params: Promise.resolve({ jobId: 'missing-job' }), - }) - - expect(response.status).toBe(404) - }) -}) diff --git a/apps/sim/app/api/jobs/[jobId]/route.ts b/apps/sim/app/api/jobs/[jobId]/route.ts deleted file mode 100644 index 01677e506ee..00000000000 --- a/apps/sim/app/api/jobs/[jobId]/route.ts +++ /dev/null @@ -1,90 +0,0 @@ -import { createLogger } from '@sim/logger' -import { toError } from '@sim/utils/errors' -import { type NextRequest, NextResponse } from 'next/server' -import { getJobStatusContract } from '@/lib/api/contracts/common' -import { parseRequest } from '@/lib/api/server' -import { checkHybridAuth } from '@/lib/auth/hybrid' -import { getJobQueue } from '@/lib/core/async-jobs' -import { generateRequestId } from '@/lib/core/utils/request' -import { withRouteHandler } from '@/lib/core/utils/with-route-handler' -import { createErrorResponse } from '@/app/api/workflows/utils' - -const logger = createLogger('TaskStatusAPI') - -export const GET = withRouteHandler( - async (request: NextRequest, context: { params: Promise<{ jobId: string }> }) => { - const parsed = await parseRequest(getJobStatusContract, request, context) - if (!parsed.success) return parsed.response - const { jobId: taskId } = parsed.data.params - const requestId = generateRequestId() - - try { - const authResult = await checkHybridAuth(request, { requireWorkflowId: false }) - if (!authResult.success || !authResult.userId) { - logger.warn(`[${requestId}] Unauthorized task status request`) - return createErrorResponse(authResult.error || 'Authentication required', 401) - } - - const authenticatedUserId = authResult.userId - - const jobQueue = await getJobQueue() - const job = await jobQueue.getJob(taskId) - - if (!job) { - return createErrorResponse('Task not found', 404) - } - - const metadataToCheck = job.metadata - - if (metadataToCheck?.workflowId) { - const { authorizeWorkflowByWorkspacePermission } = await import( - '@sim/platform-authz/workflow' - ) - const accessCheck = await authorizeWorkflowByWorkspacePermission({ - userId: authenticatedUserId, - workflowId: metadataToCheck.workflowId as string, - action: 'read', - }) - if (!accessCheck.allowed) { - logger.warn(`[${requestId}] Access denied to workflow ${metadataToCheck.workflowId}`) - return createErrorResponse('Access denied', 403) - } - - if (authResult.apiKeyType === 'workspace' && authResult.workspaceId) { - const { getWorkflowById } = await import('@/lib/workflows/utils') - const workflow = await getWorkflowById(metadataToCheck.workflowId as string) - if (!workflow?.workspaceId || workflow.workspaceId !== authResult.workspaceId) { - return createErrorResponse('API key is not authorized for this workspace', 403) - } - } - } else if (metadataToCheck?.userId && metadataToCheck.userId !== authenticatedUserId) { - logger.warn(`[${requestId}] Access denied to user ${metadataToCheck.userId}`) - return createErrorResponse('Access denied', 403) - } else if (!metadataToCheck?.userId && !metadataToCheck?.workflowId) { - logger.warn(`[${requestId}] Access denied to job ${taskId}`) - return createErrorResponse('Access denied', 403) - } - - const response: Record = { - success: true, - taskId, - status: job.status, - metadata: job.metadata, - } - - if (job.output !== undefined) response.output = job.output - if (job.error !== undefined) response.error = job.error - - return NextResponse.json(response) - } catch (error: unknown) { - const errorMessage = toError(error).message - logger.error(`[${requestId}] Error fetching task status:`, error) - - if (errorMessage?.includes('not found')) { - return createErrorResponse('Task not found', 404) - } - - return createErrorResponse('Failed to fetch task status', 500) - } - } -) diff --git a/apps/sim/app/api/resume/[workflowId]/[executionId]/[contextId]/route.test.ts b/apps/sim/app/api/resume/[workflowId]/[executionId]/[contextId]/route.test.ts index ddc55bf4df9..a1bf0f30c2b 100644 --- a/apps/sim/app/api/resume/[workflowId]/[executionId]/[contextId]/route.test.ts +++ b/apps/sim/app/api/resume/[workflowId]/[executionId]/[contextId]/route.test.ts @@ -9,6 +9,7 @@ const { mockGetCurrentPayer, mockGetPauseContextDetail, mockGetPausedExecutionDetail, + mockEnqueueResume, mockPreprocessExecution, mockValidateWorkflowAccess, } = vi.hoisted(() => ({ @@ -16,6 +17,7 @@ const { mockGetCurrentPayer: vi.fn(), mockGetPauseContextDetail: vi.fn(), mockGetPausedExecutionDetail: vi.fn(), + mockEnqueueResume: vi.fn().mockResolvedValue('resume-execution:resume-execution-1'), mockPreprocessExecution: vi.fn(), mockValidateWorkflowAccess: vi.fn(), })) @@ -28,6 +30,14 @@ vi.mock('@/lib/execution/preprocessing', () => ({ preprocessExecution: mockPreprocessExecution, })) +vi.mock('@/lib/core/async-jobs', () => ({ + getJobQueue: vi.fn().mockResolvedValue({ enqueue: mockEnqueueResume }), +})) + +vi.mock('@/lib/workflows/executor/enqueue-execution', () => ({ + RESUME_EXECUTION_JOB_ID_PREFIX: 'resume-execution:', +})) + vi.mock('@sim/utils/id', () => ({ generateId: () => 'resume-preflight-1', })) @@ -84,6 +94,7 @@ interface PausedExecutionOverrides { snapshotWorkspaceId?: string snapshotActorUserId?: string billingAttribution?: unknown + executionMode?: 'sync' | 'stream' | 'async' } function createPausedExecution(overrides: PausedExecutionOverrides = {}) { @@ -108,7 +119,7 @@ function createPausedExecution(overrides: PausedExecutionOverrides = {}) { triggerType: 'manual', useDraftState: false, startTime: '2026-07-10T00:00:00.000Z', - executionMode: 'sync', + executionMode: overrides.executionMode ?? 'sync', }, workflow: { version: '1', blocks: [], connections: [] }, input: {}, @@ -229,6 +240,41 @@ describe('POST /api/resume/[workflowId]/[executionId]/[contextId]', () => { }) }) + it('returns the resume execution ID as the only public async polling handle', async () => { + mockGetPausedExecutionDetail.mockResolvedValueOnce( + createPausedExecution({ executionMode: 'async' }) + ) + mockEnqueueOrStartResume.mockResolvedValueOnce({ + status: 'started', + resumeExecutionId: 'resume-execution-1', + resumeEntryId: 'resume-entry-1', + pausedExecution: { id: 'paused-execution-1' }, + contextId: CONTEXT_ID, + resumeInput: { approved: true }, + userId: 'current-api-key-user', + }) + const { request, context } = makeRequest() + + const response = await POST(request, context) + + expect(response.status).toBe(202) + await expect(response.json()).resolves.toEqual({ + success: true, + async: true, + executionId: 'resume-execution-1', + message: 'Resume execution queued', + statusUrl: 'https://test.sim.ai/api/workflows/workflow-1/executions/resume-execution-1', + }) + expect(mockEnqueueResume).toHaveBeenCalledWith( + 'resume-execution', + expect.objectContaining({ resumeExecutionId: 'resume-execution-1' }), + expect.objectContaining({ + jobId: 'resume-execution:resume-execution-1', + metadata: expect.objectContaining({ workflowId: WORKFLOW_ID }), + }) + ) + }) + it.each([ { statusCode: 402, message: 'Member usage limit reached', retryable: false }, { statusCode: 429, message: 'Target concurrency full', retryable: true }, diff --git a/apps/sim/app/api/resume/[workflowId]/[executionId]/[contextId]/route.ts b/apps/sim/app/api/resume/[workflowId]/[executionId]/[contextId]/route.ts index fc87a1237da..6a3ffa5ab5c 100644 --- a/apps/sim/app/api/resume/[workflowId]/[executionId]/[contextId]/route.ts +++ b/apps/sim/app/api/resume/[workflowId]/[executionId]/[contextId]/route.ts @@ -19,6 +19,7 @@ import { SSE_HEADERS } from '@/lib/core/utils/sse' import { getBaseUrl } from '@/lib/core/utils/urls' import { withRouteHandler } from '@/lib/core/utils/with-route-handler' import { preprocessExecution } from '@/lib/execution/preprocessing' +import { RESUME_EXECUTION_JOB_ID_PREFIX } from '@/lib/workflows/executor/enqueue-execution' import { PauseResumeManager } from '@/lib/workflows/executor/human-in-the-loop-manager' import { agentStreamProtocolResponseHeaders, @@ -322,14 +323,15 @@ export const POST = withRouteHandler( parentExecutionId: executionId, } - let jobId: string + let queueJobId: string try { const jobQueue = await getJobQueue() - jobId = await jobQueue.enqueue('resume-execution', resumePayload, { + queueJobId = await jobQueue.enqueue('resume-execution', resumePayload, { + jobId: `${RESUME_EXECUTION_JOB_ID_PREFIX}${enqueueResult.resumeExecutionId}`, metadata: { workflowId, workspaceId: workflow.workspaceId, userId }, }) logger.info('Enqueued async resume execution', { - jobId, + jobId: queueJobId, resumeExecutionId: enqueueResult.resumeExecutionId, }) } catch (dispatchError) { @@ -355,10 +357,9 @@ export const POST = withRouteHandler( { success: true, async: true, - jobId, executionId: enqueueResult.resumeExecutionId, message: 'Resume execution queued', - statusUrl: `${getBaseUrl()}/api/jobs/${jobId}`, + statusUrl: `${getBaseUrl()}/api/workflows/${workflowId}/executions/${enqueueResult.resumeExecutionId}`, }, { status: 202 } ) diff --git a/apps/sim/app/api/v2/workflows/[id]/executions/[executionId]/route.test.ts b/apps/sim/app/api/v2/workflows/[id]/executions/[executionId]/route.test.ts index c0e96fc8080..bfd1a3896d8 100644 --- a/apps/sim/app/api/v2/workflows/[id]/executions/[executionId]/route.test.ts +++ b/apps/sim/app/api/v2/workflows/[id]/executions/[executionId]/route.test.ts @@ -4,13 +4,13 @@ import { createMockRequest, workflowAuthzMockFns } from '@sim/testing' import { beforeEach, describe, expect, it, vi } from 'vitest' -const { mockAuthenticateV1Request, mockGetJob, mockGetWorkflowExecutionStatus, mockCancel } = - vi.hoisted(() => ({ +const { mockAuthenticateV1Request, mockGetWorkflowExecutionStatus, mockCancel } = vi.hoisted( + () => ({ mockAuthenticateV1Request: vi.fn(), - mockGetJob: vi.fn(), mockGetWorkflowExecutionStatus: vi.fn(), mockCancel: vi.fn(), - })) + }) +) vi.mock('@/app/api/v1/auth', () => ({ authenticateV1Request: mockAuthenticateV1Request, @@ -28,14 +28,6 @@ vi.mock('@/lib/execution/cancel-workflow-execution', () => ({ cancelWorkflowExecution: mockCancel, })) -vi.mock('@/lib/core/async-jobs', () => ({ - getJobQueue: vi.fn().mockResolvedValue({ getJob: mockGetJob }), -})) - -vi.mock('@/lib/workflows/executor/enqueue-execution', () => ({ - WORKFLOW_EXECUTION_JOB_ID_PREFIX: 'workflow-execution:', -})) - vi.mock('@/app/api/v2/lib/gate', () => ({ v2ApiGateError: vi.fn().mockResolvedValue(null), })) @@ -100,23 +92,31 @@ describe('v2 executions status + cancel', () => { expect(body.data.durationMs).toBe(5000) }) - it('backfills queued status from the job queue before the log row exists', async () => { - mockGetWorkflowExecutionStatus.mockResolvedValue(null) - mockGetJob.mockResolvedValue({ - status: 'pending', - metadata: { workflowId: 'workflow-1' }, + it('returns the queued execution resource before the log row exists', async () => { + mockGetWorkflowExecutionStatus.mockResolvedValue({ + executionId: 'exec-1', + workflowId: 'workflow-1', + status: 'queued', + trigger: 'api', + level: 'info', + startedAt: '2026-07-31T00:00:00.000Z', + endedAt: null, + totalDurationMs: null, + paused: null, + cost: null, + error: null, + finalOutput: null, + blockOutputs: null, }) const res = await callStatus() expect(res.status).toBe(200) expect((await res.json()).data.status).toBe('queued') - expect(mockGetJob).toHaveBeenCalledWith('workflow-execution:exec-1') }) it('404s when neither a log row nor a matching job exists', async () => { mockGetWorkflowExecutionStatus.mockResolvedValue(null) - mockGetJob.mockResolvedValue(null) const res = await callStatus() diff --git a/apps/sim/app/api/v2/workflows/[id]/executions/[executionId]/route.ts b/apps/sim/app/api/v2/workflows/[id]/executions/[executionId]/route.ts index d38da03a1b5..f32da31bfb3 100644 --- a/apps/sim/app/api/v2/workflows/[id]/executions/[executionId]/route.ts +++ b/apps/sim/app/api/v2/workflows/[id]/executions/[executionId]/route.ts @@ -1,18 +1,13 @@ import { createLogger } from '@sim/logger' import { getErrorMessage } from '@sim/utils/errors' import type { NextRequest } from 'next/server' -import { - type V2WorkflowExecutionStatus, - v2GetWorkflowExecutionContract, -} from '@/lib/api/contracts/v2/workflows' +import { v2GetWorkflowExecutionContract } from '@/lib/api/contracts/v2/workflows' import { parseRequest } from '@/lib/api/server' -import { getJobQueue } from '@/lib/core/async-jobs' import { withRouteHandler } from '@/lib/core/utils/with-route-handler' import { FUNCTIONAL_OUTPUTS_UNAVAILABLE_MESSAGE, FunctionalOutputsUnavailableError, } from '@/lib/logs/execution/functional-outputs' -import { WORKFLOW_EXECUTION_JOB_ID_PREFIX } from '@/lib/workflows/executor/enqueue-execution' import { getWorkflowExecutionStatus } from '@/lib/workflows/executor/execution-status' import { v2Data, v2Error, v2ValidationError } from '@/app/api/v2/lib/response' import { resolveV2WorkflowAccess } from '@/app/api/v2/workflows/lib/access' @@ -22,25 +17,6 @@ const logger = createLogger('V2WorkflowExecutionStatusAPI') export const dynamic = 'force-dynamic' -/** - * Maps the async job's phase onto the execution status enum for the window - * before the worker writes the durable log row. - */ -function jobStatusToExecutionStatus(jobStatus: string): V2WorkflowExecutionStatus['status'] | null { - switch (jobStatus) { - case 'pending': - return 'queued' - case 'processing': - return 'running' - case 'failed': - return 'failed' - case 'completed': - return 'completed' - default: - return null - } -} - /** * GET /api/v2/workflows/[id]/executions/[executionId] — the single status URL * for both sync and async runs. When no log row exists yet, the async job @@ -70,50 +46,23 @@ export const GET = withRouteHandler( selectedOutputs, }) - if (status) { - return v2Data({ - executionId: status.executionId, - workflowId: status.workflowId, - status: status.status, - trigger: status.trigger ?? null, - startedAt: status.startedAt, - endedAt: status.endedAt, - durationMs: status.totalDurationMs, - paused: status.paused, - cost: status.cost, - error: status.error ? classifyExecutionError(new Error(status.error)) : null, - output: status.finalOutput, - blockOutputs: status.blockOutputs, - }) - } - - // No log row yet — a queued/just-started async run. Backfilled from the - // job queue via the deterministic id; authz already ran above. - const jobQueue = await getJobQueue() - const job = await jobQueue.getJob(`${WORKFLOW_EXECUTION_JOB_ID_PREFIX}${executionId}`) - const jobWorkflowId = - job?.metadata && typeof job.metadata === 'object' - ? (job.metadata as { workflowId?: string }).workflowId - : undefined - const mapped = job ? jobStatusToExecutionStatus(job.status) : null - if (!job || jobWorkflowId !== workflowId || !mapped) { + if (!status) { return v2Error('NOT_FOUND', 'Execution not found') } return v2Data({ - executionId, - workflowId, - status: mapped, - trigger: 'api', - startedAt: null, - endedAt: null, - durationMs: null, - paused: null, - cost: null, - error: - mapped === 'failed' && job.error ? classifyExecutionError(new Error(job.error)) : null, - output: null, - blockOutputs: null, + executionId: status.executionId, + workflowId: status.workflowId, + status: status.status, + trigger: status.trigger ?? null, + startedAt: status.startedAt, + endedAt: status.endedAt, + durationMs: status.totalDurationMs, + paused: status.paused, + cost: status.cost, + error: status.error ? classifyExecutionError(new Error(status.error)) : null, + output: status.finalOutput, + blockOutputs: status.blockOutputs, }) } catch (error) { if (error instanceof FunctionalOutputsUnavailableError) { diff --git a/apps/sim/app/api/workflows/[id]/execute/route.async.test.ts b/apps/sim/app/api/workflows/[id]/execute/route.async.test.ts index 72d0f71e04d..a073abdd8bf 100644 --- a/apps/sim/app/api/workflows/[id]/execute/route.async.test.ts +++ b/apps/sim/app/api/workflows/[id]/execute/route.async.test.ts @@ -806,7 +806,10 @@ describe('workflow execute async route', () => { expect(response.status).toBe(202) expect(body.executionId).toBe('execution-123') - expect(body.jobId).toBe('job-123') + expect(body).not.toHaveProperty('jobId') + expect(body.statusUrl).toBe( + 'http://localhost:3000/api/workflows/workflow-1/executions/execution-123' + ) expect(mockClaimExecutionId).toHaveBeenCalledWith('execution-123') expect(mockEnqueue).toHaveBeenCalledWith( 'workflow-execution', diff --git a/apps/sim/app/api/workflows/[id]/execute/route.ts b/apps/sim/app/api/workflows/[id]/execute/route.ts index 9817acc81b0..7a638f1c47b 100644 --- a/apps/sim/app/api/workflows/[id]/execute/route.ts +++ b/apps/sim/app/api/workflows/[id]/execute/route.ts @@ -429,10 +429,9 @@ async function handleAsyncExecution(params: AsyncExecutionParams): Promise { if (!info) return '' const endpoint = getBaseEndpoint() + if (!endpoint.endsWith('/execute')) { + throw new Error(`Invalid workflow execution endpoint: ${endpoint}`) + } const baseUrl = endpoint.split('/api/workflows/')[0] + const statusEndpoint = `${endpoint.slice(0, -'/execute'.length)}/executions/EXECUTION_ID_FROM_EXECUTION` const payload = getPayloadObject() const isPublic = info.isPublicApi @@ -288,8 +292,8 @@ ${isPublic ? '' : ' "X-API-Key": os.environ.get("SIM_API_KEY"),\n'} json=${JSON.stringify(payload, null, 4).replace(/\n/g, '\n ')} ) -job = response.json() -print(job) # Contains jobId and executionId` +execution = response.json() +print(execution)` case 'javascript': return `const response = await fetch("${endpoint}", { @@ -301,8 +305,8 @@ ${isPublic ? '' : ' "X-API-Key": process.env.SIM_API_KEY,\n'} "Content-Typ body: JSON.stringify(${JSON.stringify(payload)}) }); -const job = await response.json(); -console.log(job); // Contains jobId and executionId` +const execution = await response.json(); +console.log(execution);` case 'typescript': return `const response = await fetch("${endpoint}", { @@ -314,8 +318,8 @@ ${isPublic ? '' : ' "X-API-Key": process.env.SIM_API_KEY,\n'} "Content-Typ body: JSON.stringify(${JSON.stringify(payload)}) }); -const job: { jobId: string; executionId: string } = await response.json(); -console.log(job); // Contains jobId and executionId` +const execution: { executionId: string; statusUrl: string } = await response.json(); +console.log(execution);` default: return '' @@ -325,14 +329,15 @@ console.log(job); // Contains jobId and executionId` switch (language) { case 'curl': return `curl -H "X-API-Key: $SIM_API_KEY" \\ - ${baseUrl}/api/jobs/JOB_ID_FROM_EXECUTION` + "${statusEndpoint}?includeOutput=true"` case 'python': return `import os import requests response = requests.get( - "${baseUrl}/api/jobs/JOB_ID_FROM_EXECUTION", + "${statusEndpoint}", + params={"includeOutput": "true"}, headers={"X-API-Key": os.environ.get("SIM_API_KEY")} ) @@ -341,7 +346,7 @@ print(status)` case 'javascript': return `const response = await fetch( - "${baseUrl}/api/jobs/JOB_ID_FROM_EXECUTION", + "${statusEndpoint}?includeOutput=true", { headers: { "X-API-Key": process.env.SIM_API_KEY } } @@ -352,7 +357,7 @@ console.log(status);` case 'typescript': return `const response = await fetch( - "${baseUrl}/api/jobs/JOB_ID_FROM_EXECUTION", + "${statusEndpoint}?includeOutput=true", { headers: { "X-API-Key": process.env.SIM_API_KEY } } @@ -417,13 +422,13 @@ console.log(limits);` const getAsyncExampleTitle = () => { switch (asyncExampleType) { case 'execute': - return 'Execute Job' + return 'Start Execution' case 'status': return 'Check Status' case 'rate-limits': return 'Usage Limits' default: - return 'Execute Job' + return 'Start Execution' } } @@ -562,7 +567,7 @@ console.log(limits);` size='sm' className='!w-fit !py-0.5 min-w-[100px] rounded-md px-[9px]' options={[ - { label: 'Execute Job', value: 'execute' }, + { label: 'Start Execution', value: 'execute' }, { label: 'Check Status', value: 'status' }, { label: 'Usage Limits', value: 'rate-limits' }, ]} diff --git a/apps/sim/lib/api/contracts/common.ts b/apps/sim/lib/api/contracts/common.ts index d79833499db..6d2227d51d0 100644 --- a/apps/sim/lib/api/contracts/common.ts +++ b/apps/sim/lib/api/contracts/common.ts @@ -1,5 +1,4 @@ import { z } from 'zod' -import { jobIdParamsSchema } from '@/lib/api/contracts/primitives' import { defineRouteContract } from '@/lib/api/contracts/types' const NO_EMAIL_HEADER_CONTROL_CHARS_REGEX = /^[^\r\n\u0000-\u001F\u007F]+$/ @@ -105,26 +104,3 @@ export const getStatusContract = defineRouteContract({ }), }, }) - -const jobStatusSchema = z.enum(['pending', 'processing', 'completed', 'failed']) - -const jobStatusResponseSchema = z - .object({ - success: z.literal(true), - taskId: z.string(), - status: jobStatusSchema, - metadata: z.record(z.string(), z.unknown()).nullable().optional(), - output: z.unknown().optional(), - error: z.string().optional(), - }) - .passthrough() - -export const getJobStatusContract = defineRouteContract({ - method: 'GET', - path: '/api/jobs/[jobId]', - params: jobIdParamsSchema, - response: { - mode: 'json', - schema: jobStatusResponseSchema, - }, -}) diff --git a/apps/sim/lib/api/contracts/primitives.ts b/apps/sim/lib/api/contracts/primitives.ts index 142e0990a54..b66db42f85a 100644 --- a/apps/sim/lib/api/contracts/primitives.ts +++ b/apps/sim/lib/api/contracts/primitives.ts @@ -81,10 +81,6 @@ export function isCanonicalBase64(value: string): boolean { return true } -export const jobIdParamsSchema = z.object({ - jobId: z.string().min(1), -}) - /** * Non-empty string identifier with no custom message — suitable for internal * shapes where the field name is not worth surfacing. For a required *request* diff --git a/apps/sim/lib/api/contracts/workflows.ts b/apps/sim/lib/api/contracts/workflows.ts index 8847adc6630..e71a29a46bd 100644 --- a/apps/sim/lib/api/contracts/workflows.ts +++ b/apps/sim/lib/api/contracts/workflows.ts @@ -546,6 +546,7 @@ const pausedWorkflowExecutionDetailSchema = pausedWorkflowExecutionSummarySchema }) const workflowExecutionStatusEnum = z.enum([ + 'queued', 'pending', 'running', 'paused', @@ -633,7 +634,6 @@ const resumeWorkflowExecutionContextResponseSchema = z async: z.boolean().optional(), executionId: z.string().optional(), queuePosition: z.number().optional(), - jobId: z.string().optional(), output: z.unknown().optional(), error: z.string().optional(), metadata: z diff --git a/apps/sim/lib/compare/data/sim.ts b/apps/sim/lib/compare/data/sim.ts index b2a46f0b893..b70b87bbc8c 100644 --- a/apps/sim/lib/compare/data/sim.ts +++ b/apps/sim/lib/compare/data/sim.ts @@ -1086,10 +1086,10 @@ export const simProfile: CompetitorProfile = { }, asyncExecution: { value: - 'Yes: a workflow can be triggered in fire-and-forget async mode, returning HTTP 202 with a job ID immediately, then polled via a dedicated jobs endpoint through queued/processing/completed/failed states', + 'Yes: a workflow can be triggered in fire-and-forget async mode, returning HTTP 202 with an execution ID immediately, then polled through the canonical execution resource across queued/running/terminal states', detail: - 'Async jobs are tracked via polling the job endpoint rather than a completion webhook/callback option.', - shortValue: 'Async mode: job ID returned immediately, poll for result', + 'Async runs are tracked by execution ID through the same execution status endpoint used for durable logs rather than a separate queue-job resource.', + shortValue: 'Async mode: execution ID returned immediately, poll for result', confidence: 'verified', sources: [ { @@ -1098,8 +1098,8 @@ export const simProfile: CompetitorProfile = { asOf: '2026-07-02', }, { - url: 'https://github.com/simstudioai/sim/blob/main/apps/sim/app/api/jobs/[jobId]/route.ts', - label: 'Sim codebase: async job status endpoint', + url: 'https://github.com/simstudioai/sim/blob/main/apps/sim/app/api/workflows/[id]/executions/[executionId]/route.ts', + label: 'Sim codebase: execution status endpoint', asOf: '2026-07-02', }, ], diff --git a/apps/sim/lib/copilot/tools/handlers/deployment/deploy.ts b/apps/sim/lib/copilot/tools/handlers/deployment/deploy.ts index a1db42f3e4c..d93c303c232 100644 --- a/apps/sim/lib/copilot/tools/handlers/deployment/deploy.ts +++ b/apps/sim/lib/copilot/tools/handlers/deployment/deploy.ts @@ -34,6 +34,17 @@ function buildWorkflowApiEndpoint(baseUrl: string, workflowId: string): string { return `${baseUrl}/api/workflows/${workflowId}/execute` } +function buildWorkflowExecutionStatusEndpoint( + baseUrl: string, + apiEndpoint: string, + executionId: string +): string { + if (!apiEndpoint.startsWith(`${baseUrl}/api/workflows/`) || !apiEndpoint.endsWith('/execute')) { + throw new Error(`Invalid workflow execution endpoint: ${apiEndpoint}`) + } + return `${apiEndpoint.slice(0, -'/execute'.length)}/executions/${executionId}` +} + function buildWorkflowApiConfig(baseUrl: string, apiEndpoint: string) { return { endpoint: apiEndpoint, @@ -60,7 +71,11 @@ function buildWorkflowApiConfig(baseUrl: string, apiEndpoint: string) { stream: false, headers: { 'X-Execution-Mode': 'async' }, body: { input: { key: 'value' } }, - jobStatusEndpointTemplate: `${baseUrl}/api/jobs/{jobId}`, + executionStatusEndpointTemplate: buildWorkflowExecutionStatusEndpoint( + baseUrl, + apiEndpoint, + '{executionId}' + ), }, }, } @@ -81,7 +96,7 @@ function buildWorkflowApiExamples(baseUrl: string, apiEndpoint: string) { -H "X-API-Key: YOUR_API_KEY" \\ -H "X-Execution-Mode: async" \\ -d '{"input":{"key":"value"}}'`, - poll: `curl "${baseUrl}/api/jobs/JOB_ID" \\ + poll: `curl "${buildWorkflowExecutionStatusEndpoint(baseUrl, apiEndpoint, 'EXECUTION_ID')}" \\ -H "X-API-Key: YOUR_API_KEY"`, } } diff --git a/apps/sim/lib/workflows/executor/enqueue-execution.ts b/apps/sim/lib/workflows/executor/enqueue-execution.ts index 949c98be1dc..4e8992bafd4 100644 --- a/apps/sim/lib/workflows/executor/enqueue-execution.ts +++ b/apps/sim/lib/workflows/executor/enqueue-execution.ts @@ -11,6 +11,7 @@ const logger = createLogger('WorkflowEnqueueExecution') const ASYNC_ENQUEUE_ATTEMPTS = 2 export const WORKFLOW_EXECUTION_JOB_ID_PREFIX = 'workflow-execution:' +export const RESUME_EXECUTION_JOB_ID_PREFIX = 'resume-execution:' export interface EnqueueWorkflowExecutionParams { requestId: string diff --git a/apps/sim/lib/workflows/executor/execution-status.test.ts b/apps/sim/lib/workflows/executor/execution-status.test.ts new file mode 100644 index 00000000000..02fb941d580 --- /dev/null +++ b/apps/sim/lib/workflows/executor/execution-status.test.ts @@ -0,0 +1,90 @@ +/** + * @vitest-environment node + */ +import { queueTableRows, resetDbChainMock, schemaMock } from '@sim/testing' +import { beforeEach, describe, expect, it, vi } from 'vitest' + +const { mockGetJob } = vi.hoisted(() => ({ + mockGetJob: vi.fn(), +})) + +vi.mock('@/lib/core/async-jobs', () => ({ + getJobQueue: vi.fn().mockResolvedValue({ getJob: mockGetJob }), +})) + +vi.mock('@/lib/workflows/executor/enqueue-execution', () => ({ + RESUME_EXECUTION_JOB_ID_PREFIX: 'resume-execution:', + WORKFLOW_EXECUTION_JOB_ID_PREFIX: 'workflow-execution:', +})) + +import { getWorkflowExecutionStatus } from '@/lib/workflows/executor/execution-status' + +const input = { + workflowId: 'workflow-1', + executionId: 'execution-1', + includeOutput: false, + selectedOutputs: [], +} + +describe('getWorkflowExecutionStatus queue projection', () => { + beforeEach(() => { + vi.clearAllMocks() + resetDbChainMock() + queueTableRows(schemaMock.workflowExecutionLogs, []) + }) + + it('projects a queued workflow job as an execution resource', async () => { + mockGetJob.mockResolvedValue({ + status: 'pending', + createdAt: new Date('2026-08-05T12:00:00.000Z'), + metadata: { + workflowId: 'workflow-1', + correlation: { triggerType: 'api' }, + }, + }) + + const status = await getWorkflowExecutionStatus(input) + + expect(status).toMatchObject({ + executionId: 'execution-1', + workflowId: 'workflow-1', + status: 'queued', + trigger: 'api', + startedAt: '2026-08-05T12:00:00.000Z', + endedAt: null, + error: null, + }) + expect(mockGetJob).toHaveBeenCalledWith('workflow-execution:execution-1') + }) + + it('uses the resume execution ID when the queued work is a resume attempt', async () => { + mockGetJob.mockResolvedValueOnce(null).mockResolvedValueOnce({ + status: 'processing', + createdAt: new Date('2026-08-05T12:00:00.000Z'), + startedAt: new Date('2026-08-05T12:00:01.000Z'), + metadata: { workflowId: 'workflow-1' }, + }) + + const status = await getWorkflowExecutionStatus(input) + + expect(status).toMatchObject({ + executionId: 'execution-1', + workflowId: 'workflow-1', + status: 'running', + startedAt: '2026-08-05T12:00:01.000Z', + }) + expect(mockGetJob).toHaveBeenNthCalledWith(2, 'resume-execution:execution-1') + }) + + it('does not expose a queue record belonging to another workflow', async () => { + mockGetJob + .mockResolvedValueOnce({ + status: 'pending', + createdAt: new Date('2026-08-05T12:00:00.000Z'), + metadata: { workflowId: 'workflow-2' }, + }) + .mockResolvedValueOnce(null) + + await expect(getWorkflowExecutionStatus(input)).resolves.toBeNull() + }) +}) diff --git a/apps/sim/lib/workflows/executor/execution-status.ts b/apps/sim/lib/workflows/executor/execution-status.ts index d608dd3d825..80f12184650 100644 --- a/apps/sim/lib/workflows/executor/execution-status.ts +++ b/apps/sim/lib/workflows/executor/execution-status.ts @@ -2,11 +2,16 @@ import { db } from '@sim/db' import { pausedExecutions, workflowExecutionLogs } from '@sim/db/schema' import { and, eq } from 'drizzle-orm' import type { WorkflowExecutionStatusResponse } from '@/lib/api/contracts/workflows' +import { getJobQueue } from '@/lib/core/async-jobs' import { collectFunctionalBlockOutputs, type FunctionalExecutionDataSource, } from '@/lib/logs/execution/functional-outputs' import { materializeExecutionData } from '@/lib/logs/execution/trace-store' +import { + RESUME_EXECUTION_JOB_ID_PREFIX, + WORKFLOW_EXECUTION_JOB_ID_PREFIX, +} from '@/lib/workflows/executor/enqueue-execution' import { getAutomaticResumeWaitingMetadata } from '@/lib/workflows/executor/paused-execution-metadata' import type { PausePoint } from '@/executor/types' @@ -14,7 +19,9 @@ import type { PausePoint } from '@/executor/types' * Reads a single execution's status resource — the log row, the paused-state * overlay, and (when requested) materialized outputs. Extracted so the v1 and * v2 status routes render the identical resource from one read path. - * Auth is the caller's responsibility. Returns `null` when no log row exists. + * Auth is the caller's responsibility. Before a worker writes the durable log + * row, the deterministic queue record is projected as the same execution + * resource so callers never need a separate job identifier or endpoint. */ type LogStatus = 'pending' | 'running' | 'completed' | 'failed' | 'cancelled' @@ -114,7 +121,41 @@ export async function getWorkflowExecutionStatus( ) .limit(1) - if (!logRow) return null + if (!logRow) { + const jobQueue = await getJobQueue() + const jobIds = [ + `${WORKFLOW_EXECUTION_JOB_ID_PREFIX}${executionId}`, + `${RESUME_EXECUTION_JOB_ID_PREFIX}${executionId}`, + ] + + for (const jobId of jobIds) { + const job = await jobQueue.getJob(jobId) + if (!job || job.metadata.workflowId !== workflowId) continue + + const status: WorkflowExecutionStatusResponse['status'] = + job.status === 'pending' ? 'queued' : job.status === 'processing' ? 'running' : job.status + const startedAt = job.startedAt ?? job.createdAt + const endedAt = job.completedAt ?? null + + return { + executionId, + workflowId, + status, + trigger: job.metadata.correlation?.triggerType ?? 'api', + level: status === 'failed' ? 'error' : 'info', + startedAt: startedAt.toISOString(), + endedAt: endedAt?.toISOString() ?? null, + totalDurationMs: endedAt ? endedAt.getTime() - startedAt.getTime() : null, + paused: null, + cost: null, + error: status === 'failed' ? (job.error ?? 'Execution failed') : null, + finalOutput: null, + blockOutputs: null, + } + } + + return null + } const [pausedRow] = await db .select({ diff --git a/packages/python-sdk/README.md b/packages/python-sdk/README.md index 2690f635a17..f4ecbc680ac 100644 --- a/packages/python-sdk/README.md +++ b/packages/python-sdk/README.md @@ -115,17 +115,25 @@ result = client.execute_workflow_sync("workflow-id", {"data": "some input"}, tim **Returns:** `WorkflowExecutionResult` -##### get_job_status(job_id) +##### get_workflow_execution(workflow_id, execution_id, *, include_output=None, selected_outputs=None) -Get the status of an async job. +Get the status and optional outputs of a workflow execution. Use the execution ID returned by async execution. ```python -status = client.get_job_status("job-id-from-async-execution") -print("Job status:", status) +status = client.get_workflow_execution( + "workflow-id", + "execution-id", + include_output=True, + selected_outputs=["agent.content"] +) +print("Execution status:", status["status"]) ``` **Parameters:** -- `job_id` (str): The job ID returned from async execution +- `workflow_id` (str): The workflow ID +- `execution_id` (str): The execution ID returned from async execution +- `include_output` (bool, keyword-only): Include the final output for completed executions +- `selected_outputs` (list, keyword-only): Block output selectors to include **Returns:** `dict` @@ -248,9 +256,8 @@ class SimStudioError(Exception): @dataclass class AsyncExecutionResult: success: bool - job_id: str + execution_id: str status_url: str - execution_id: Optional[str] = None message: str = "" async_execution: bool = True ``` @@ -527,4 +534,4 @@ isort simstudio/ ## License -Apache-2.0 \ No newline at end of file +Apache-2.0 diff --git a/packages/python-sdk/simstudio/__init__.py b/packages/python-sdk/simstudio/__init__.py index 0e2609e2f26..1fcd255ee2c 100644 --- a/packages/python-sdk/simstudio/__init__.py +++ b/packages/python-sdk/simstudio/__init__.py @@ -49,9 +49,8 @@ class WorkflowStatus: class AsyncExecutionResult: """Result of an async workflow execution.""" success: bool - job_id: str + execution_id: str status_url: str - execution_id: Optional[str] = None message: str = "" async_execution: bool = True @@ -159,7 +158,7 @@ def execute_workflow( ) -> Union[WorkflowExecutionResult, AsyncExecutionResult]: """ Execute a workflow with optional input data. - If async_execution is True, returns immediately with a task ID. + If async_execution is True, returns immediately with an execution ID. File objects in input will be automatically detected and converted to base64. @@ -238,12 +237,11 @@ def execute_workflow( result_data = response.json() # Check if this is an async execution response (202 status) - if response.status_code == 202 and 'jobId' in result_data: + if response.status_code == 202 and 'executionId' in result_data: return AsyncExecutionResult( success=result_data.get('success', True), - job_id=result_data['jobId'], + execution_id=result_data['executionId'], status_url=result_data['statusUrl'], - execution_id=result_data.get('executionId'), message=result_data.get('message', ''), async_execution=result_data.get('async', True) ) @@ -376,23 +374,38 @@ def close(self) -> None: """Close the underlying HTTP session.""" self._session.close() - def get_job_status(self, job_id: str) -> Dict[str, Any]: + def get_workflow_execution( + self, + workflow_id: str, + execution_id: str, + *, + include_output: Optional[bool] = None, + selected_outputs: Optional[list] = None + ) -> Dict[str, Any]: """ - Get the status of an async job. + Get a workflow execution's current status and optional outputs. Args: - job_id: The job ID returned from async execution + workflow_id: The workflow ID + execution_id: The execution ID returned from async execution + include_output: Include the final output for completed executions + selected_outputs: Block output selectors to include Returns: - Dictionary containing the job status + Dictionary containing the execution status Raises: SimStudioError: If getting the status fails """ - url = f"{self.base_url}/api/jobs/{job_id}" + url = f"{self.base_url}/api/workflows/{workflow_id}/executions/{execution_id}" + params = {} + if include_output is not None: + params['includeOutput'] = str(include_output).lower() + if selected_outputs: + params['selectedOutputs'] = ','.join(selected_outputs) try: - response = self._session.get(url) + response = self._session.get(url, params=params or None) self._update_rate_limit_info(response) @@ -410,7 +423,7 @@ def get_job_status(self, job_id: str) -> Dict[str, Any]: return response.json() except requests.RequestException as e: - raise SimStudioError(f'Failed to get job status: {str(e)}', 'STATUS_ERROR') + raise SimStudioError(f'Failed to get workflow execution: {str(e)}', 'STATUS_ERROR') def execute_with_retry( self, @@ -565,4 +578,4 @@ def __exit__(self, exc_type, exc_val, exc_tb): # For backward compatibility -Client = SimStudioClient \ No newline at end of file +Client = SimStudioClient diff --git a/packages/python-sdk/tests/test_client.py b/packages/python-sdk/tests/test_client.py index 814ad7610ef..52d5802b74f 100644 --- a/packages/python-sdk/tests/test_client.py +++ b/packages/python-sdk/tests/test_client.py @@ -95,16 +95,15 @@ def test_context_manager(mock_close): @patch('simstudio.requests.Session.post') -def test_async_execution_returns_job_id(mock_post): +def test_async_execution_returns_execution_id(mock_post): """Test async execution returns AsyncExecutionResult.""" mock_response = Mock() mock_response.ok = True mock_response.status_code = 202 mock_response.json.return_value = { "success": True, - "jobId": "job-123", - "statusUrl": "https://test.sim.ai/api/jobs/job-123", "executionId": "execution-123", + "statusUrl": "https://test.sim.ai/api/workflows/workflow-id/executions/execution-123", "message": "Workflow execution started", "async": True } @@ -119,9 +118,8 @@ def test_async_execution_returns_job_id(mock_post): ) assert result.success is True - assert result.job_id == "job-123" - assert result.status_url == "https://test.sim.ai/api/jobs/job-123" assert result.execution_id == "execution-123" + assert result.status_url == "https://test.sim.ai/api/workflows/workflow-id/executions/execution-123" assert result.async_execution is True call_args = mock_post.call_args @@ -172,43 +170,46 @@ def test_async_header_not_set_when_false(mock_post): @patch('simstudio.requests.Session.get') -def test_get_job_status_success(mock_get): - """Test getting job status.""" +def test_get_workflow_execution_success(mock_get): + """Test getting workflow execution status.""" mock_response = Mock() mock_response.ok = True mock_response.json.return_value = { - "success": True, - "taskId": "task-123", + "executionId": "execution-123", + "workflowId": "workflow-123", "status": "completed", - "metadata": { - "startedAt": "2024-01-01T00:00:00Z", - "completedAt": "2024-01-01T00:01:00Z", - "duration": 60000 - }, - "output": {"result": "done"} + "finalOutput": {"result": "done"} } mock_response.headers.get.return_value = None mock_get.return_value = mock_response client = SimStudioClient(api_key="test-api-key", base_url="https://test.sim.ai") - result = client.get_job_status("task-123") + result = client.get_workflow_execution( + "workflow-123", + "execution-123", + include_output=True, + selected_outputs=["agent.content"] + ) - assert result["taskId"] == "task-123" + assert result["executionId"] == "execution-123" assert result["status"] == "completed" - assert result["output"]["result"] == "done" - mock_get.assert_called_once_with("https://test.sim.ai/api/jobs/task-123") + assert result["finalOutput"]["result"] == "done" + mock_get.assert_called_once_with( + "https://test.sim.ai/api/workflows/workflow-123/executions/execution-123", + params={"includeOutput": "true", "selectedOutputs": "agent.content"} + ) @patch('simstudio.requests.Session.get') -def test_get_job_status_not_found(mock_get): - """Test job not found error.""" +def test_get_workflow_execution_not_found(mock_get): + """Test execution not found error.""" mock_response = Mock() mock_response.ok = False mock_response.status_code = 404 mock_response.reason = "Not Found" mock_response.json.return_value = { - "error": "Job not found", - "code": "JOB_NOT_FOUND" + "error": "Execution not found", + "code": "EXECUTION_NOT_FOUND" } mock_response.headers.get.return_value = None mock_get.return_value = mock_response @@ -216,8 +217,8 @@ def test_get_job_status_not_found(mock_get): client = SimStudioClient(api_key="test-api-key") with pytest.raises(SimStudioError) as exc_info: - client.get_job_status("invalid-task") - assert "Job not found" in str(exc_info.value) + client.get_workflow_execution("workflow-123", "invalid-execution") + assert "Execution not found" in str(exc_info.value) @patch('simstudio.requests.Session.post') @@ -534,4 +535,4 @@ def test_execute_workflow_with_dict_input_spreads_at_root(mock_post): assert request_body["ticker"] == "NVDA" assert request_body["quantity"] == 100 - assert "input" not in request_body # Should not wrap in input field \ No newline at end of file + assert "input" not in request_body # Should not wrap in input field diff --git a/packages/ts-sdk/README.md b/packages/ts-sdk/README.md index 0ce547f6e51..49adb82debc 100644 --- a/packages/ts-sdk/README.md +++ b/packages/ts-sdk/README.md @@ -125,19 +125,25 @@ const result = await client.executeWorkflowSync('workflow-id', { data: 'some inp **Returns:** `Promise` -##### getJobStatus(jobId) +##### getWorkflowExecution(workflowId, executionId, options?) -Get the status of an async job. +Get the status and optional outputs of a workflow execution. Use the `executionId` returned by async execution. ```typescript -const status = await client.getJobStatus('job-id-from-async-execution'); -console.log('Job status:', status); +const status = await client.getWorkflowExecution('workflow-id', 'execution-id', { + includeOutput: true, + selectedOutputs: ['agent.content'] +}); +console.log('Execution status:', status.status); ``` **Parameters:** -- `jobId` (string): The job ID returned from async execution +- `workflowId` (string): The workflow ID +- `executionId` (string): The execution ID returned from async execution +- `options.includeOutput` (boolean, optional): Include the final output for completed executions +- `options.selectedOutputs` (string[], optional): Block output selectors to include -**Returns:** `Promise` +**Returns:** `Promise` ##### executeWithRetry(workflowId, input?, options?, retryOptions?) @@ -228,7 +234,7 @@ interface WorkflowExecutionResult { ### LargeValueRef -Oversized execution values may be returned as a versioned reference inside `output`, `logs`, streaming events, or async job status responses. +Oversized execution values may be returned as a versioned reference inside `output`, `logs`, streaming events, or execution status responses. The `key` field is an opaque execution-scoped server storage pointer, not a client-readable download URL. ```typescript @@ -268,9 +274,8 @@ class SimStudioError extends Error { ```typescript interface AsyncExecutionResult { success: boolean; - jobId: string; + executionId: string; statusUrl: string; - executionId?: string; message: string; async: true; } @@ -533,4 +538,4 @@ bun run dev ## License -Apache-2.0 \ No newline at end of file +Apache-2.0 diff --git a/packages/ts-sdk/src/index.test.ts b/packages/ts-sdk/src/index.test.ts index c5066442f99..e59c3698f99 100644 --- a/packages/ts-sdk/src/index.test.ts +++ b/packages/ts-sdk/src/index.test.ts @@ -101,8 +101,8 @@ describe('SimStudioClient', () => { status: 202, json: vi.fn().mockResolvedValue({ success: true, - jobId: 'job-123', - statusUrl: 'https://test.sim.ai/api/jobs/job-123', + executionId: 'execution-123', + statusUrl: 'https://test.sim.ai/api/workflows/workflow-id/executions/execution-123', message: 'Workflow execution queued', async: true, }), @@ -118,8 +118,11 @@ describe('SimStudioClient', () => { { async: true } ) - expect(result).toHaveProperty('jobId', 'job-123') - expect(result).toHaveProperty('statusUrl', 'https://test.sim.ai/api/jobs/job-123') + expect(result).toHaveProperty('executionId', 'execution-123') + expect(result).toHaveProperty( + 'statusUrl', + 'https://test.sim.ai/api/workflows/workflow-id/executions/execution-123' + ) expect(result).toHaveProperty('async', true) // Verify headers were set correctly @@ -176,20 +179,15 @@ describe('SimStudioClient', () => { }) }) - describe('getJobStatus', () => { - it('should fetch job status with correct endpoint', async () => { + describe('getWorkflowExecution', () => { + it('should fetch execution status and outputs from the execution resource', async () => { const mockResponse = { ok: true, json: vi.fn().mockResolvedValue({ - success: true, - taskId: 'task-123', + executionId: 'execution-123', + workflowId: 'workflow-123', status: 'completed', - metadata: { - startedAt: '2024-01-01T00:00:00Z', - completedAt: '2024-01-01T00:01:00Z', - duration: 60000, - }, - output: { result: 'done' }, + finalOutput: { result: 'done' }, }), headers: { get: vi.fn().mockReturnValue(null), @@ -197,25 +195,30 @@ describe('SimStudioClient', () => { } vi.mocked(mockFetch).mockResolvedValue(mockResponse as any) - const result = await client.getJobStatus('task-123') + const result = await client.getWorkflowExecution('workflow-123', 'execution-123', { + includeOutput: true, + selectedOutputs: ['agent.content'], + }) - expect(result).toHaveProperty('taskId', 'task-123') + expect(result).toHaveProperty('executionId', 'execution-123') expect(result).toHaveProperty('status', 'completed') - expect(result).toHaveProperty('output') + expect(result).toHaveProperty('finalOutput') // Verify correct endpoint was called const calls = vi.mocked(mockFetch).mock.calls - expect(calls[0][0]).toBe('https://test.sim.ai/api/jobs/task-123') + expect(calls[0][0]).toBe( + 'https://test.sim.ai/api/workflows/workflow-123/executions/execution-123?includeOutput=true&selectedOutputs=agent.content' + ) }) - it('should handle job not found error', async () => { + it('should handle execution not found errors', async () => { const mockResponse = { ok: false, status: 404, statusText: 'Not Found', json: vi.fn().mockResolvedValue({ - error: 'Job not found', - code: 'JOB_NOT_FOUND', + error: 'Execution not found', + code: 'EXECUTION_NOT_FOUND', }), headers: { get: vi.fn().mockReturnValue(null), @@ -223,8 +226,12 @@ describe('SimStudioClient', () => { } vi.mocked(mockFetch).mockResolvedValue(mockResponse as any) - await expect(client.getJobStatus('invalid-task')).rejects.toThrow(SimStudioError) - await expect(client.getJobStatus('invalid-task')).rejects.toThrow('Job not found') + await expect( + client.getWorkflowExecution('workflow-123', 'invalid-execution') + ).rejects.toThrow(SimStudioError) + await expect( + client.getWorkflowExecution('workflow-123', 'invalid-execution') + ).rejects.toThrow('Execution not found') }) }) diff --git a/packages/ts-sdk/src/index.ts b/packages/ts-sdk/src/index.ts index d1538ff5e84..b38b7dfae3a 100644 --- a/packages/ts-sdk/src/index.ts +++ b/packages/ts-sdk/src/index.ts @@ -45,19 +45,31 @@ export interface ExecutionOptions { export interface AsyncExecutionResult { success: boolean - jobId: string + executionId: string statusUrl: string - executionId?: string message: string async: true } -export interface JobStatusResult { - taskId: string - status: string - metadata?: Record - output?: unknown - error?: string +export interface WorkflowExecutionStatus { + executionId: string + workflowId: string + status: 'queued' | 'pending' | 'running' | 'paused' | 'completed' | 'failed' | 'cancelled' + trigger: string + level: string + startedAt: string + endedAt: string | null + totalDurationMs: number | null + paused: Record | null + cost: { total: number } | null + error: string | null + finalOutput: unknown | null + blockOutputs: Record | null +} + +export interface GetWorkflowExecutionOptions { + includeOutput?: boolean + selectedOutputs?: string[] } export interface RateLimitInfo { @@ -374,11 +386,22 @@ export class SimStudioClient { } /** - * Get the status of an async job - * @param taskId The job ID returned from async execution + * Get a workflow execution's current status and optional outputs. */ - async getJobStatus(taskId: string): Promise { - const url = `${this.baseUrl}/api/jobs/${taskId}` + async getWorkflowExecution( + workflowId: string, + executionId: string, + options: GetWorkflowExecutionOptions = {} + ): Promise { + const query = new URLSearchParams() + if (options.includeOutput !== undefined) { + query.set('includeOutput', String(options.includeOutput)) + } + if (options.selectedOutputs?.length) { + query.set('selectedOutputs', options.selectedOutputs.join(',')) + } + const queryString = query.toString() + const url = `${this.baseUrl}/api/workflows/${workflowId}/executions/${executionId}${queryString ? `?${queryString}` : ''}` try { const response = await fetch(url, { @@ -400,13 +423,16 @@ export class SimStudioClient { } const result = await response.json() - return result as JobStatusResult + return result as WorkflowExecutionStatus } catch (error: any) { if (error instanceof SimStudioError) { throw error } - throw new SimStudioError(describeError(error) || 'Failed to get job status', 'STATUS_ERROR') + throw new SimStudioError( + describeError(error) || 'Failed to get workflow execution', + 'STATUS_ERROR' + ) } } From 6236bb2a7f98bd50776d7fbf7f4dcc934d7bf03e Mon Sep 17 00:00:00 2001 From: Theodore Li Date: Wed, 5 Aug 2026 13:55:20 -0700 Subject: [PATCH 2/8] fix(api): preserve legacy jobs while preferring v2 executions --- .../docs/de/api-reference/getting-started.mdx | 14 +- .../content/docs/de/api-reference/python.mdx | 66 +++---- .../docs/de/api-reference/typescript.mdx | 71 ++++--- apps/docs/content/docs/de/sdks/python.mdx | 66 +++---- apps/docs/content/docs/de/sdks/typescript.mdx | 71 ++++--- .../(generated)/execution/meta.json | 2 +- .../docs/en/api-reference/getting-started.mdx | 76 +++----- .../content/docs/en/api-reference/python.mdx | 24 ++- .../docs/en/api-reference/typescript.mdx | 24 ++- .../en/workflows/blocks/human-in-the-loop.mdx | 36 ++-- .../docs/en/workflows/deployment/api.mdx | 86 ++++----- .../docs/es/api-reference/getting-started.mdx | 14 +- .../content/docs/es/api-reference/python.mdx | 66 +++---- .../docs/es/api-reference/typescript.mdx | 71 ++++--- apps/docs/content/docs/es/sdks/python.mdx | 66 +++---- apps/docs/content/docs/es/sdks/typescript.mdx | 71 ++++--- .../docs/fr/api-reference/getting-started.mdx | 14 +- .../content/docs/fr/api-reference/python.mdx | 66 +++---- .../docs/fr/api-reference/typescript.mdx | 71 ++++--- apps/docs/content/docs/fr/sdks/python.mdx | 66 +++---- apps/docs/content/docs/fr/sdks/typescript.mdx | 71 ++++--- .../docs/ja/api-reference/getting-started.mdx | 14 +- .../content/docs/ja/api-reference/python.mdx | 66 +++---- .../docs/ja/api-reference/typescript.mdx | 71 ++++--- apps/docs/content/docs/ja/sdks/python.mdx | 66 +++---- apps/docs/content/docs/ja/sdks/typescript.mdx | 71 ++++--- .../docs/zh/api-reference/getting-started.mdx | 14 +- .../content/docs/zh/api-reference/python.mdx | 59 +++--- .../docs/zh/api-reference/typescript.mdx | 62 +++---- apps/docs/content/docs/zh/sdks/python.mdx | 66 +++---- apps/docs/content/docs/zh/sdks/typescript.mdx | 71 ++++--- apps/docs/openapi-core.json | 135 +++++++++++++- apps/docs/openapi.json | 135 +++++++++++++- apps/sim/app/api/jobs/[jobId]/route.test.ts | 102 ++++++++++ apps/sim/app/api/jobs/[jobId]/route.ts | 90 +++++++++ .../[executionId]/[contextId]/route.test.ts | 2 +- .../[executionId]/[contextId]/route.ts | 2 +- .../[id]/execute/route.async.test.ts | 6 +- .../app/api/workflows/[id]/execute/route.ts | 3 +- .../deploy-modal/components/api/api.tsx | 44 ++--- .../components/deploy-modal/deploy-modal.tsx | 18 +- apps/sim/lib/api/contracts/common.ts | 24 +++ apps/sim/lib/api/contracts/primitives.ts | 4 + apps/sim/lib/api/contracts/workflows.ts | 1 + .../tools/handlers/deployment/deploy.ts | 13 +- packages/python-sdk/README.md | 16 +- packages/python-sdk/simstudio/__init__.py | 106 +++++++---- packages/python-sdk/tests/test_client.py | 149 ++++++++++----- packages/ts-sdk/README.md | 16 +- packages/ts-sdk/src/index.test.ts | 174 ++++++++++-------- packages/ts-sdk/src/index.ts | 150 ++++++++++++--- 51 files changed, 1721 insertions(+), 1141 deletions(-) create mode 100644 apps/sim/app/api/jobs/[jobId]/route.test.ts create mode 100644 apps/sim/app/api/jobs/[jobId]/route.ts diff --git a/apps/docs/content/docs/de/api-reference/getting-started.mdx b/apps/docs/content/docs/de/api-reference/getting-started.mdx index fec1ab44c46..7e94ab0d7bd 100644 --- a/apps/docs/content/docs/de/api-reference/getting-started.mdx +++ b/apps/docs/content/docs/de/api-reference/getting-started.mdx @@ -109,27 +109,27 @@ curl -X POST https://www.sim.ai/api/workflows/{workflowId}/execute \ -d '{"inputs": {}, "async": true}' ``` -This returns immediately with an `executionId` and `statusUrl`: +This returns immediately with a `jobId` and `statusUrl`: ```json { "success": true, - "executionId": "c7a92e15-3f4b-4d8c-a1e6-9b0d5f2c8e74", - "statusUrl": "https://www.sim.ai/api/workflows/{workflowId}/executions/c7a92e15-3f4b-4d8c-a1e6-9b0d5f2c8e74", - "message": "Workflow execution queued", + "jobId": "job_abc123", + "statusUrl": "https://www.sim.ai/api/jobs/job_abc123", + "message": "Workflow execution started", "async": true } ``` -Poll the [Get Execution Status](/api-reference/execution/getWorkflowExecution) endpoint until the status is terminal: +Poll the [Get Job Status](/api-reference/execution/getJobStatus) endpoint until the status is `completed` or `failed`: ```bash -curl https://www.sim.ai/api/workflows/{workflowId}/executions/{executionId}?includeOutput=true \ +curl https://www.sim.ai/api/jobs/{jobId} \ -H "X-API-Key: YOUR_API_KEY" ``` - Execution status transitions follow: `queued` → `running` → `completed`, `failed`, `cancelled`, or `paused`. The `finalOutput` field is populated for completed executions when `includeOutput=true`. + Job status transitions follow: `queued` → `processing` → `completed` or `failed`. The `output` field is only present when status is `completed`. ## Response Format diff --git a/apps/docs/content/docs/de/api-reference/python.mdx b/apps/docs/content/docs/de/api-reference/python.mdx index 76f220da394..64e1370f87d 100644 --- a/apps/docs/content/docs/de/api-reference/python.mdx +++ b/apps/docs/content/docs/de/api-reference/python.mdx @@ -112,34 +112,30 @@ if is_ready: **Rückgabe:** `bool` -##### get_workflow_execution() +##### get_job_status() -Get the status and optional outputs of a workflow execution. +Ruft den Status einer asynchronen Job-Ausführung ab. ```python -status = client.get_workflow_execution("workflow-id", "execution-id", include_output=True) -print("Status:", status["status"]) # 'queued', 'running', 'completed', 'failed' +status = client.get_job_status("task-id-from-async-execution") +print("Status:", status["status"]) # 'queued', 'processing', 'completed', 'failed' if status["status"] == "completed": - print("Output:", status["finalOutput"]) + print("Output:", status["output"]) ``` -**Parameters:** -- `workflow_id` (str): The workflow ID -- `execution_id` (str): The execution ID returned from async execution -- `include_output` (bool, optional): Include the final output for completed executions -- `selected_outputs` (list[str], optional): Block output selectors to include +**Parameter:** +- `task_id` (str): Die Task-ID, die von der asynchronen Ausführung zurückgegeben wurde -**Returns:** `Dict[str, Any]` +**Rückgabe:** `Dict[str, Any]` -**Response fields:** -- `executionId` (str): The execution ID -- `workflowId` (str): The workflow ID -- `status` (str): One of `'queued'`, `'pending'`, `'running'`, `'paused'`, `'completed'`, `'failed'`, `'cancelled'` -- `startedAt` / `endedAt` (str): Execution timestamps -- `totalDurationMs` (int, optional): Duration in milliseconds -- `finalOutput` (any, optional): The workflow output when requested for a completed execution -- `blockOutputs` (dict, optional): Requested block outputs -- `error` (str, optional): Failure details +**Antwortfelder:** +- `success` (bool): Ob die Anfrage erfolgreich war +- `taskId` (str): Die Task-ID +- `status` (str): Einer von `'queued'`, `'processing'`, `'completed'`, `'failed'`, `'cancelled'` +- `metadata` (dict): Enthält `startedAt`, `completedAt` und `duration` +- `output` (any, optional): Die Workflow-Ausgabe (wenn abgeschlossen) +- `error` (any, optional): Fehlerdetails (wenn fehlgeschlagen) +- `estimatedDuration` (int, optional): Geschätzte Dauer in Millisekunden (wenn in Bearbeitung/in Warteschlange) ##### execute_with_retry() @@ -275,10 +271,10 @@ class WorkflowExecutionResult: @dataclass class AsyncExecutionResult: success: bool - execution_id: str - status_url: str - message: str = "" - async_execution: bool = True + task_id: str + status: str # 'queued' + created_at: str + links: Dict[str, str] # e.g., {"status": "/api/jobs/{taskId}"} ``` ### WorkflowStatus @@ -493,31 +489,27 @@ def execute_async(): # Start async execution result = client.execute_workflow( "workflow-id", - input={"data": "large dataset"}, + input_data={"data": "large dataset"}, async_execution=True # Execute asynchronously ) # Check if result is an async execution - if hasattr(result, 'async_execution') and result.async_execution: - print(f"Execution ID: {result.execution_id}") - print(f"Status endpoint: {result.status_url}") + if hasattr(result, 'task_id'): + print(f"Task ID: {result.task_id}") + print(f"Status endpoint: {result.links['status']}") # Poll for completion - status = client.get_workflow_execution( - "workflow-id", result.execution_id, include_output=True - ) + status = client.get_job_status(result.task_id) - while status["status"] in ["queued", "pending", "running"]: + while status["status"] in ["queued", "processing"]: print(f"Current status: {status['status']}") time.sleep(2) # Wait 2 seconds - status = client.get_workflow_execution( - "workflow-id", result.execution_id, include_output=True - ) + status = client.get_job_status(result.task_id) if status["status"] == "completed": print("Workflow completed!") - print(f"Output: {status['finalOutput']}") - print(f"Duration: {status['totalDurationMs']}") + print(f"Output: {status['output']}") + print(f"Duration: {status['metadata']['duration']}") else: print(f"Workflow failed: {status['error']}") diff --git a/apps/docs/content/docs/de/api-reference/typescript.mdx b/apps/docs/content/docs/de/api-reference/typescript.mdx index 060ff165701..fed552b8403 100644 --- a/apps/docs/content/docs/de/api-reference/typescript.mdx +++ b/apps/docs/content/docs/de/api-reference/typescript.mdx @@ -133,37 +133,31 @@ if (isReady) { **Rückgabewert:** `Promise` -##### getWorkflowExecution() +##### getJobStatus() -Get the status and optional outputs of a workflow execution. +Den Status einer asynchronen Job-Ausführung abrufen. ```typescript -const status = await client.getWorkflowExecution('workflow-id', 'execution-id', { - includeOutput: true -}); -console.log('Status:', status.status); // 'queued', 'running', 'completed', 'failed' +const status = await client.getJobStatus('task-id-from-async-execution'); +console.log('Status:', status.status); // 'queued', 'processing', 'completed', 'failed' if (status.status === 'completed') { - console.log('Output:', status.finalOutput); + console.log('Output:', status.output); } ``` -**Parameters:** -- `workflowId` (string): The workflow ID -- `executionId` (string): The execution ID returned from async execution -- `options.includeOutput` (boolean, optional): Include the final output for completed executions -- `options.selectedOutputs` (string[], optional): Block output selectors to include +**Parameter:** +- `taskId` (string): Die Task-ID, die von der asynchronen Ausführung zurückgegeben wurde -**Returns:** `Promise` +**Rückgabewert:** `Promise` -**Response fields:** -- `executionId` (string): The execution ID -- `workflowId` (string): The workflow ID -- `status` (string): One of `'queued'`, `'pending'`, `'running'`, `'paused'`, `'completed'`, `'failed'`, `'cancelled'` -- `startedAt` / `endedAt` (string): Execution timestamps -- `totalDurationMs` (number, nullable): Duration in milliseconds -- `finalOutput` (any, nullable): The workflow output when requested for a completed execution -- `blockOutputs` (object, nullable): Requested block outputs -- `error` (string, nullable): Failure details +**Antwortfelder:** +- `success` (boolean): Ob die Anfrage erfolgreich war +- `taskId` (string): Die Task-ID +- `status` (string): Einer der Werte `'queued'`, `'processing'`, `'completed'`, `'failed'`, `'cancelled'` +- `metadata` (object): Enthält `startedAt`, `completedAt` und `duration` +- `output` (any, optional): Die Workflow-Ausgabe (wenn abgeschlossen) +- `error` (any, optional): Fehlerdetails (wenn fehlgeschlagen) +- `estimatedDuration` (number, optional): Geschätzte Dauer in Millisekunden (wenn in Bearbeitung/in der Warteschlange) ##### executeWithRetry() @@ -292,10 +286,12 @@ interface WorkflowExecutionResult { ```typescript interface AsyncExecutionResult { success: boolean; - executionId: string; - statusUrl: string; - message: string; - async: true; + taskId: string; + status: 'queued'; + createdAt: string; + links: { + status: string; // e.g., "/api/jobs/{taskId}" + }; } ``` @@ -795,32 +791,29 @@ const client = new SimStudioClient({ async function executeAsync() { try { // Start async execution - const result = await client.executeWorkflow('workflow-id', { data: 'large dataset' }, { + const result = await client.executeWorkflow('workflow-id', { + input: { data: 'large dataset' }, async: true // Execute asynchronously }); // Check if result is an async execution - if ('async' in result && result.async) { - console.log('Execution ID:', result.executionId); - console.log('Status endpoint:', result.statusUrl); + if ('taskId' in result) { + console.log('Task ID:', result.taskId); + console.log('Status endpoint:', result.links.status); // Poll for completion - let status = await client.getWorkflowExecution('workflow-id', result.executionId, { - includeOutput: true - }); + let status = await client.getJobStatus(result.taskId); - while (status.status === 'queued' || status.status === 'pending' || status.status === 'running') { + while (status.status === 'queued' || status.status === 'processing') { console.log('Current status:', status.status); await new Promise(resolve => setTimeout(resolve, 2000)); // Wait 2 seconds - status = await client.getWorkflowExecution('workflow-id', result.executionId, { - includeOutput: true - }); + status = await client.getJobStatus(result.taskId); } if (status.status === 'completed') { console.log('Workflow completed!'); - console.log('Output:', status.finalOutput); - console.log('Duration:', status.totalDurationMs); + console.log('Output:', status.output); + console.log('Duration:', status.metadata.duration); } else { console.error('Workflow failed:', status.error); } diff --git a/apps/docs/content/docs/de/sdks/python.mdx b/apps/docs/content/docs/de/sdks/python.mdx index 76f220da394..64e1370f87d 100644 --- a/apps/docs/content/docs/de/sdks/python.mdx +++ b/apps/docs/content/docs/de/sdks/python.mdx @@ -112,34 +112,30 @@ if is_ready: **Rückgabe:** `bool` -##### get_workflow_execution() +##### get_job_status() -Get the status and optional outputs of a workflow execution. +Ruft den Status einer asynchronen Job-Ausführung ab. ```python -status = client.get_workflow_execution("workflow-id", "execution-id", include_output=True) -print("Status:", status["status"]) # 'queued', 'running', 'completed', 'failed' +status = client.get_job_status("task-id-from-async-execution") +print("Status:", status["status"]) # 'queued', 'processing', 'completed', 'failed' if status["status"] == "completed": - print("Output:", status["finalOutput"]) + print("Output:", status["output"]) ``` -**Parameters:** -- `workflow_id` (str): The workflow ID -- `execution_id` (str): The execution ID returned from async execution -- `include_output` (bool, optional): Include the final output for completed executions -- `selected_outputs` (list[str], optional): Block output selectors to include +**Parameter:** +- `task_id` (str): Die Task-ID, die von der asynchronen Ausführung zurückgegeben wurde -**Returns:** `Dict[str, Any]` +**Rückgabe:** `Dict[str, Any]` -**Response fields:** -- `executionId` (str): The execution ID -- `workflowId` (str): The workflow ID -- `status` (str): One of `'queued'`, `'pending'`, `'running'`, `'paused'`, `'completed'`, `'failed'`, `'cancelled'` -- `startedAt` / `endedAt` (str): Execution timestamps -- `totalDurationMs` (int, optional): Duration in milliseconds -- `finalOutput` (any, optional): The workflow output when requested for a completed execution -- `blockOutputs` (dict, optional): Requested block outputs -- `error` (str, optional): Failure details +**Antwortfelder:** +- `success` (bool): Ob die Anfrage erfolgreich war +- `taskId` (str): Die Task-ID +- `status` (str): Einer von `'queued'`, `'processing'`, `'completed'`, `'failed'`, `'cancelled'` +- `metadata` (dict): Enthält `startedAt`, `completedAt` und `duration` +- `output` (any, optional): Die Workflow-Ausgabe (wenn abgeschlossen) +- `error` (any, optional): Fehlerdetails (wenn fehlgeschlagen) +- `estimatedDuration` (int, optional): Geschätzte Dauer in Millisekunden (wenn in Bearbeitung/in Warteschlange) ##### execute_with_retry() @@ -275,10 +271,10 @@ class WorkflowExecutionResult: @dataclass class AsyncExecutionResult: success: bool - execution_id: str - status_url: str - message: str = "" - async_execution: bool = True + task_id: str + status: str # 'queued' + created_at: str + links: Dict[str, str] # e.g., {"status": "/api/jobs/{taskId}"} ``` ### WorkflowStatus @@ -493,31 +489,27 @@ def execute_async(): # Start async execution result = client.execute_workflow( "workflow-id", - input={"data": "large dataset"}, + input_data={"data": "large dataset"}, async_execution=True # Execute asynchronously ) # Check if result is an async execution - if hasattr(result, 'async_execution') and result.async_execution: - print(f"Execution ID: {result.execution_id}") - print(f"Status endpoint: {result.status_url}") + if hasattr(result, 'task_id'): + print(f"Task ID: {result.task_id}") + print(f"Status endpoint: {result.links['status']}") # Poll for completion - status = client.get_workflow_execution( - "workflow-id", result.execution_id, include_output=True - ) + status = client.get_job_status(result.task_id) - while status["status"] in ["queued", "pending", "running"]: + while status["status"] in ["queued", "processing"]: print(f"Current status: {status['status']}") time.sleep(2) # Wait 2 seconds - status = client.get_workflow_execution( - "workflow-id", result.execution_id, include_output=True - ) + status = client.get_job_status(result.task_id) if status["status"] == "completed": print("Workflow completed!") - print(f"Output: {status['finalOutput']}") - print(f"Duration: {status['totalDurationMs']}") + print(f"Output: {status['output']}") + print(f"Duration: {status['metadata']['duration']}") else: print(f"Workflow failed: {status['error']}") diff --git a/apps/docs/content/docs/de/sdks/typescript.mdx b/apps/docs/content/docs/de/sdks/typescript.mdx index 060ff165701..fed552b8403 100644 --- a/apps/docs/content/docs/de/sdks/typescript.mdx +++ b/apps/docs/content/docs/de/sdks/typescript.mdx @@ -133,37 +133,31 @@ if (isReady) { **Rückgabewert:** `Promise` -##### getWorkflowExecution() +##### getJobStatus() -Get the status and optional outputs of a workflow execution. +Den Status einer asynchronen Job-Ausführung abrufen. ```typescript -const status = await client.getWorkflowExecution('workflow-id', 'execution-id', { - includeOutput: true -}); -console.log('Status:', status.status); // 'queued', 'running', 'completed', 'failed' +const status = await client.getJobStatus('task-id-from-async-execution'); +console.log('Status:', status.status); // 'queued', 'processing', 'completed', 'failed' if (status.status === 'completed') { - console.log('Output:', status.finalOutput); + console.log('Output:', status.output); } ``` -**Parameters:** -- `workflowId` (string): The workflow ID -- `executionId` (string): The execution ID returned from async execution -- `options.includeOutput` (boolean, optional): Include the final output for completed executions -- `options.selectedOutputs` (string[], optional): Block output selectors to include +**Parameter:** +- `taskId` (string): Die Task-ID, die von der asynchronen Ausführung zurückgegeben wurde -**Returns:** `Promise` +**Rückgabewert:** `Promise` -**Response fields:** -- `executionId` (string): The execution ID -- `workflowId` (string): The workflow ID -- `status` (string): One of `'queued'`, `'pending'`, `'running'`, `'paused'`, `'completed'`, `'failed'`, `'cancelled'` -- `startedAt` / `endedAt` (string): Execution timestamps -- `totalDurationMs` (number, nullable): Duration in milliseconds -- `finalOutput` (any, nullable): The workflow output when requested for a completed execution -- `blockOutputs` (object, nullable): Requested block outputs -- `error` (string, nullable): Failure details +**Antwortfelder:** +- `success` (boolean): Ob die Anfrage erfolgreich war +- `taskId` (string): Die Task-ID +- `status` (string): Einer der Werte `'queued'`, `'processing'`, `'completed'`, `'failed'`, `'cancelled'` +- `metadata` (object): Enthält `startedAt`, `completedAt` und `duration` +- `output` (any, optional): Die Workflow-Ausgabe (wenn abgeschlossen) +- `error` (any, optional): Fehlerdetails (wenn fehlgeschlagen) +- `estimatedDuration` (number, optional): Geschätzte Dauer in Millisekunden (wenn in Bearbeitung/in der Warteschlange) ##### executeWithRetry() @@ -292,10 +286,12 @@ interface WorkflowExecutionResult { ```typescript interface AsyncExecutionResult { success: boolean; - executionId: string; - statusUrl: string; - message: string; - async: true; + taskId: string; + status: 'queued'; + createdAt: string; + links: { + status: string; // e.g., "/api/jobs/{taskId}" + }; } ``` @@ -795,32 +791,29 @@ const client = new SimStudioClient({ async function executeAsync() { try { // Start async execution - const result = await client.executeWorkflow('workflow-id', { data: 'large dataset' }, { + const result = await client.executeWorkflow('workflow-id', { + input: { data: 'large dataset' }, async: true // Execute asynchronously }); // Check if result is an async execution - if ('async' in result && result.async) { - console.log('Execution ID:', result.executionId); - console.log('Status endpoint:', result.statusUrl); + if ('taskId' in result) { + console.log('Task ID:', result.taskId); + console.log('Status endpoint:', result.links.status); // Poll for completion - let status = await client.getWorkflowExecution('workflow-id', result.executionId, { - includeOutput: true - }); + let status = await client.getJobStatus(result.taskId); - while (status.status === 'queued' || status.status === 'pending' || status.status === 'running') { + while (status.status === 'queued' || status.status === 'processing') { console.log('Current status:', status.status); await new Promise(resolve => setTimeout(resolve, 2000)); // Wait 2 seconds - status = await client.getWorkflowExecution('workflow-id', result.executionId, { - includeOutput: true - }); + status = await client.getJobStatus(result.taskId); } if (status.status === 'completed') { console.log('Workflow completed!'); - console.log('Output:', status.finalOutput); - console.log('Duration:', status.totalDurationMs); + console.log('Output:', status.output); + console.log('Duration:', status.metadata.duration); } else { console.error('Workflow failed:', status.error); } diff --git a/apps/docs/content/docs/en/api-reference/(generated)/execution/meta.json b/apps/docs/content/docs/en/api-reference/(generated)/execution/meta.json index 1a9a9283917..52458d430c3 100644 --- a/apps/docs/content/docs/en/api-reference/(generated)/execution/meta.json +++ b/apps/docs/content/docs/en/api-reference/(generated)/execution/meta.json @@ -1,3 +1,3 @@ { - "pages": ["executeWorkflow", "getWorkflowExecution", "cancelExecution"] + "pages": ["executeWorkflow", "getWorkflowExecution", "cancelExecution", "getJobStatus"] } diff --git a/apps/docs/content/docs/en/api-reference/getting-started.mdx b/apps/docs/content/docs/en/api-reference/getting-started.mdx index e593d678ff9..2b744d8586d 100644 --- a/apps/docs/content/docs/en/api-reference/getting-started.mdx +++ b/apps/docs/content/docs/en/api-reference/getting-started.mdx @@ -49,28 +49,28 @@ A workflow must be deployed before it can be executed via the API. Click the **D ```bash - curl -X POST https://www.sim.ai/api/workflows/{workflowId}/execute \ + curl -X POST https://www.sim.ai/api/v2/workflows/{workflowId}/execute \ -H "Content-Type: application/json" \ -H "X-API-Key: YOUR_API_KEY" \ - -d '{"inputs": {}}' + -d '{"input": {}}' ``` ```typescript const response = await fetch( - `https://www.sim.ai/api/workflows/${workflowId}/execute`, + `https://www.sim.ai/api/v2/workflows/${workflowId}/execute`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-API-Key': process.env.SIM_API_KEY!, }, - body: JSON.stringify({ inputs: {} }), + body: JSON.stringify({ input: {} }), } ) const data = await response.json() - console.log(data.output) + console.log(data.data.output) ``` @@ -79,16 +79,16 @@ A workflow must be deployed before it can be executed via the API. Click the **D import os response = requests.post( - f"https://www.sim.ai/api/workflows/{workflow_id}/execute", + f"https://www.sim.ai/api/v2/workflows/{workflow_id}/execute", headers={ "Content-Type": "application/json", "X-API-Key": os.environ["SIM_API_KEY"], }, - json={"inputs": {}}, + json={"input": {}}, ) data = response.json() - print(data["output"]) + print(data["data"]["output"]) ``` @@ -103,77 +103,61 @@ By default, workflow executions are **synchronous** — the API blocks until the For long-running workflows, use **asynchronous execution** by passing `async: true`: ```bash -curl -X POST https://www.sim.ai/api/workflows/{workflowId}/execute \ +curl -X POST https://www.sim.ai/api/v2/workflows/{workflowId}/execute \ -H "Content-Type: application/json" \ -H "X-API-Key: YOUR_API_KEY" \ - -d '{"inputs": {}, "async": true}' + -d '{"input": {}, "async": true}' ``` This returns immediately with an `executionId` and `statusUrl`: ```json { - "success": true, - "executionId": "c7a92e15-3f4b-4d8c-a1e6-9b0d5f2c8e74", - "statusUrl": "https://www.sim.ai/api/workflows/{workflowId}/executions/c7a92e15-3f4b-4d8c-a1e6-9b0d5f2c8e74", - "message": "Workflow execution queued", - "async": true + "data": { + "executionId": "c7a92e15-3f4b-4d8c-a1e6-9b0d5f2c8e74", + "statusUrl": "https://www.sim.ai/api/v2/workflows/{workflowId}/executions/c7a92e15-3f4b-4d8c-a1e6-9b0d5f2c8e74" + } } ``` Poll the [Get Execution Status](/api-reference/execution/getWorkflowExecution) endpoint until the status is terminal: ```bash -curl https://www.sim.ai/api/workflows/{workflowId}/executions/{executionId}?includeOutput=true \ +curl https://www.sim.ai/api/v2/workflows/{workflowId}/executions/{executionId}?includeOutput=true \ -H "X-API-Key: YOUR_API_KEY" ``` - Execution status transitions follow: `queued` → `running` → `completed`, `failed`, `cancelled`, or `paused`. The `finalOutput` field is populated for completed executions when `includeOutput=true`. + Execution status transitions follow: `queued` → `running` → `completed`, `failed`, `cancelled`, or `paused`. The `data.output` field is populated for completed executions when `includeOutput=true`. ## Response Format -Successful responses include an `output` object with your workflow results and a `limits` object with your current rate limit and usage status: +Successful v2 responses wrap the execution resource in `data`: ```json { - "success": true, - "output": { - "result": "Hello, world!" - }, - "limits": { - "workflowExecutionRateLimit": { - "sync": { - "requestsPerMinute": 60, - "maxBurst": 10, - "remaining": 59, - "resetAt": "2025-01-01T00:01:00Z" - }, - "async": { - "requestsPerMinute": 30, - "maxBurst": 5, - "remaining": 30, - "resetAt": "2025-01-01T00:01:00Z" - } - }, - "usage": { - "currentPeriodCost": 1.25, - "limit": 50.00, - "plan": "pro", - "isExceeded": false - } + "data": { + "executionId": "c7a92e15-3f4b-4d8c-a1e6-9b0d5f2c8e74", + "workflowId": "{workflowId}", + "status": "completed", + "output": { "result": "Hello, world!" }, + "error": null, + "durationMs": 842 } } ``` ## Error Handling -The API uses standard HTTP status codes. Error responses include a human-readable `error` message: +The API uses standard HTTP status codes. v2 errors include a stable code and human-readable message: ```json { - "error": "Workflow not found" + "error": { + "code": "NOT_FOUND", + "message": "Workflow not found" + } } ``` @@ -191,7 +175,7 @@ The API uses standard HTTP status codes. Error responses include a human-readabl ## Rate Limits -Rate limits depend on your subscription plan and apply separately to synchronous and asynchronous executions. Every execution response includes a `limits` object showing your current rate limit status. +Rate limits depend on your subscription plan and apply separately to synchronous and asynchronous executions. When rate limited, the API returns a `429` response with a `Retry-After` header indicating how many seconds to wait before retrying. diff --git a/apps/docs/content/docs/en/api-reference/python.mdx b/apps/docs/content/docs/en/api-reference/python.mdx index caab508b76b..b00d4e88eeb 100644 --- a/apps/docs/content/docs/en/api-reference/python.mdx +++ b/apps/docs/content/docs/en/api-reference/python.mdx @@ -120,7 +120,7 @@ Get the status and optional outputs of a workflow execution. status = client.get_workflow_execution("workflow-id", "execution-id", include_output=True) print("Status:", status["status"]) # 'queued', 'running', 'completed', 'failed' if status["status"] == "completed": - print("Output:", status["finalOutput"]) + print("Output:", status["output"]) ``` **Parameters:** @@ -136,10 +136,18 @@ if status["status"] == "completed": - `workflowId` (str): The workflow ID - `status` (str): One of `'queued'`, `'pending'`, `'running'`, `'paused'`, `'completed'`, `'failed'`, `'cancelled'` - `startedAt` / `endedAt` (str): Execution timestamps -- `totalDurationMs` (int, optional): Duration in milliseconds -- `finalOutput` (any, optional): The workflow output when requested for a completed execution +- `durationMs` (int, optional): Duration in milliseconds +- `output` (any, optional): The workflow output when requested for a completed execution - `blockOutputs` (dict, optional): Requested block outputs -- `error` (str, optional): Failure details +- `error` (dict, optional): Structured failure details with `code`, `message`, and optional `details` + +##### get_job_status() + +Get the status of a job created through the legacy async execution endpoint. New integrations should use `get_workflow_execution()` with the execution ID instead. + +```python +status = client.get_job_status("legacy-job-id") +``` ##### execute_with_retry() @@ -515,8 +523,8 @@ def execute_async(): if status["status"] == "completed": print("Workflow completed!") - print(f"Output: {status['finalOutput']}") - print(f"Duration: {status['totalDurationMs']}") + print(f"Output: {status['output']}") + print(f"Duration: {status['durationMs']}") else: print(f"Workflow failed: {status['error']}") @@ -663,13 +671,13 @@ def stream_workflow(): def generate(): response = requests.post( - 'https://sim.ai/api/workflows/WORKFLOW_ID/execute', + 'https://sim.ai/api/v2/workflows/WORKFLOW_ID/execute', headers={ 'Content-Type': 'application/json', 'X-API-Key': os.getenv('SIM_API_KEY') }, json={ - 'message': 'Generate a story', + 'input': {'message': 'Generate a story'}, 'stream': True, 'selectedOutputs': ['agent1.content'] }, diff --git a/apps/docs/content/docs/en/api-reference/typescript.mdx b/apps/docs/content/docs/en/api-reference/typescript.mdx index 6741c4a6cfb..9f18bbb0d3c 100644 --- a/apps/docs/content/docs/en/api-reference/typescript.mdx +++ b/apps/docs/content/docs/en/api-reference/typescript.mdx @@ -136,7 +136,7 @@ const status = await client.getWorkflowExecution('workflow-id', 'execution-id', }); console.log('Status:', status.status); // 'queued', 'running', 'completed', 'failed' if (status.status === 'completed') { - console.log('Output:', status.finalOutput); + console.log('Output:', status.output); } ``` @@ -153,10 +153,18 @@ if (status.status === 'completed') { - `workflowId` (string): The workflow ID - `status` (string): One of `'queued'`, `'pending'`, `'running'`, `'paused'`, `'completed'`, `'failed'`, `'cancelled'` - `startedAt` / `endedAt` (string): Execution timestamps -- `totalDurationMs` (number, nullable): Duration in milliseconds -- `finalOutput` (any, nullable): The workflow output when requested for a completed execution +- `durationMs` (number, nullable): Duration in milliseconds +- `output` (any, nullable): The workflow output when requested for a completed execution - `blockOutputs` (object, nullable): Requested block outputs -- `error` (string, nullable): Failure details +- `error` (object, nullable): Structured failure details with `code`, `message`, and optional `details` + +##### getJobStatus() + +Get the status of a job created through the legacy async execution endpoint. New integrations should use `getWorkflowExecution()` with the execution ID instead. + +```typescript +const status = await client.getJobStatus('legacy-job-id'); +``` ##### executeWithRetry() @@ -790,8 +798,8 @@ async function executeAsync() { if (status.status === 'completed') { console.log('Workflow completed!'); - console.log('Output:', status.finalOutput); - console.log('Duration:', status.totalDurationMs); + console.log('Output:', status.output); + console.log('Duration:', status.durationMs); } else { console.error('Workflow failed:', status.error); } @@ -940,14 +948,14 @@ function StreamingWorkflow() { // IMPORTANT: Make this API call from your backend server, not the browser // Never expose your API key in client-side code - const response = await fetch('https://sim.ai/api/workflows/WORKFLOW_ID/execute', { + const response = await fetch('https://sim.ai/api/v2/workflows/WORKFLOW_ID/execute', { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-API-Key': process.env.SIM_API_KEY! // Server-side environment variable only }, body: JSON.stringify({ - message: 'Generate a story', + input: { message: 'Generate a story' }, stream: true, selectedOutputs: ['agent1.content'] }) diff --git a/apps/docs/content/docs/en/workflows/blocks/human-in-the-loop.mdx b/apps/docs/content/docs/en/workflows/blocks/human-in-the-loop.mdx index 37925b74bd1..de583ceb48b 100644 --- a/apps/docs/content/docs/en/workflows/blocks/human-in-the-loop.mdx +++ b/apps/docs/content/docs/en/workflows/blocks/human-in-the-loop.mdx @@ -121,7 +121,7 @@ Access resume data in downstream blocks using ``. - **Stream mode** (`stream: true` on the original execute call) — The resume response streams SSE events with `selectedOutputs` chunks, just like the initial execution. - - **Async mode** (`X-Execution-Mode: async` on the original execute call) — The resume dispatches execution to a background worker and returns immediately with `202`, including the resume attempt's `executionId` and `statusUrl` for polling: + - **Async mode** (`async: true` on the original v2 execute call) — The resume dispatches execution to a background worker and returns immediately with `202`, including the resume attempt's `executionId` and v2 `statusUrl` for polling: ```json { @@ -129,7 +129,7 @@ Access resume data in downstream blocks using ``. "async": true, "executionId": "", "message": "Resume execution queued", - "statusUrl": "/api/workflows//executions/" + "statusUrl": "/api/v2/workflows//executions/" } ``` @@ -138,7 +138,7 @@ Access resume data in downstream blocks using ``. Poll the `statusUrl` from the async response to check when the resume completes: ```bash - GET /api/workflows/{workflowId}/executions/{resumeExecutionId}?includeOutput=true + GET /api/v2/workflows/{workflowId}/executions/{resumeExecutionId}?includeOutput=true X-API-Key: your-api-key ``` @@ -162,7 +162,7 @@ Access resume data in downstream blocks using ``. ## API Execute Behavior -When triggering a workflow via the execute API (`POST /api/workflows/{id}/execute`), HITL blocks cause the execution to pause and return the `_resume` data in the response: +When triggering a workflow through `POST /api/v2/workflows/{id}/execute`, HITL blocks cause the execution to pause and return the `_resume` data in the v2 response envelope. The legacy `POST /api/workflows/{id}/execute` endpoint remains available for existing integrations. @@ -170,19 +170,23 @@ When triggering a workflow via the execute API (`POST /api/workflows/{id}/execut ```json { - "success": true, - "executionId": "", - "output": { - "data": { - "operation": "human", - "_resume": { - "apiUrl": "/api/resume/{workflowId}/{executionId}/{contextId}", - "uiUrl": "/resume/{workflowId}/{executionId}", - "contextId": "", - "executionId": "", - "workflowId": "" + "data": { + "executionId": "", + "workflowId": "", + "status": "paused", + "output": { + "data": { + "operation": "human", + "_resume": { + "apiUrl": "/api/resume/{workflowId}/{executionId}/{contextId}", + "uiUrl": "/resume/{workflowId}/{executionId}", + "contextId": "", + "executionId": "", + "workflowId": "" + } } - } + }, + "error": null } } ``` diff --git a/apps/docs/content/docs/en/workflows/deployment/api.mdx b/apps/docs/content/docs/en/workflows/deployment/api.mdx index 0e71726bd06..ad35c338f9d 100644 --- a/apps/docs/content/docs/en/workflows/deployment/api.mdx +++ b/apps/docs/content/docs/en/workflows/deployment/api.mdx @@ -26,7 +26,7 @@ Click **Deploy** to publish your workflow for the first time, or **Update** to p Once deployed, your workflow is available at: ``` -POST https://sim.ai/api/workflows/{workflow-id}/execute +POST https://sim.ai/api/v2/workflows/{workflow-id}/execute ``` @@ -96,10 +96,10 @@ At the bottom of the tab, two buttons give you quick access to key settings: By default, API endpoints require an API key passed in the `x-api-key` header. Generate keys in **Settings → Sim Keys** or via the **Generate API Key** button in the API tab. ```bash -curl -X POST https://sim.ai/api/workflows/{workflow-id}/execute \ +curl -X POST https://sim.ai/api/v2/workflows/{workflow-id}/execute \ -H "Content-Type: application/json" \ -H "x-api-key: $SIM_API_KEY" \ - -d '{ "input": "Hello" }' + -d '{ "input": { "message": "Hello" } }' ``` ### API Info and Public Access @@ -128,10 +128,10 @@ The default mode. Send a request and wait for the complete response: ```bash -curl -X POST https://sim.ai/api/workflows/{workflow-id}/execute \ +curl -X POST https://sim.ai/api/v2/workflows/{workflow-id}/execute \ -H "Content-Type: application/json" \ -H "x-api-key: $SIM_API_KEY" \ - -d '{ "input": "Summarize this article" }' + -d '{ "input": { "message": "Summarize this article" } }' ``` @@ -139,25 +139,25 @@ curl -X POST https://sim.ai/api/workflows/{workflow-id}/execute \ import requests, os response = requests.post( - "https://sim.ai/api/workflows/{workflow-id}/execute", + "https://sim.ai/api/v2/workflows/{workflow-id}/execute", headers={ "Content-Type": "application/json", "x-api-key": os.environ["SIM_API_KEY"] }, - json={"input": "Summarize this article"} + json={"input": {"message": "Summarize this article"}} ) print(response.json()) ``` ```typescript -const response = await fetch('https://sim.ai/api/workflows/{workflow-id}/execute', { +const response = await fetch('https://sim.ai/api/v2/workflows/{workflow-id}/execute', { method: 'POST', headers: { 'Content-Type': 'application/json', 'x-api-key': process.env.SIM_API_KEY! }, - body: JSON.stringify({ input: 'Summarize this article' }) + body: JSON.stringify({ input: { message: 'Summarize this article' } }) }); console.log(await response.json()); ``` @@ -179,11 +179,11 @@ The `selectedOutputs` values in the request body follow the format `blockName.fi ```bash -curl -X POST https://sim.ai/api/workflows/{workflow-id}/execute \ +curl -X POST https://sim.ai/api/v2/workflows/{workflow-id}/execute \ -H "Content-Type: application/json" \ -H "x-api-key: $SIM_API_KEY" \ -d '{ - "input": "Write a long essay", + "input": { "prompt": "Write a long essay" }, "stream": true, "selectedOutputs": ["agent_1.content"] }' @@ -194,13 +194,13 @@ curl -X POST https://sim.ai/api/workflows/{workflow-id}/execute \ import requests, os response = requests.post( - "https://sim.ai/api/workflows/{workflow-id}/execute", + "https://sim.ai/api/v2/workflows/{workflow-id}/execute", headers={ "Content-Type": "application/json", "x-api-key": os.environ["SIM_API_KEY"] }, json={ - "input": "Write a long essay", + "input": {"prompt": "Write a long essay"}, "stream": True, "selectedOutputs": ["agent_1.content"] }, @@ -213,14 +213,14 @@ for line in response.iter_lines(): ```typescript -const response = await fetch('https://sim.ai/api/workflows/{workflow-id}/execute', { +const response = await fetch('https://sim.ai/api/v2/workflows/{workflow-id}/execute', { method: 'POST', headers: { 'Content-Type': 'application/json', 'x-api-key': process.env.SIM_API_KEY! }, body: JSON.stringify({ - input: 'Write a long essay', + input: { prompt: 'Write a long essay' }, stream: true, selectedOutputs: ['agent_1.content'] }) @@ -242,12 +242,12 @@ while (true) { By default a streaming run carries answer text only. To also receive the Agent block's reasoning and its tool-call lifecycle, set `includeThinking` / `includeToolCalls`: ```bash -curl -N -X POST https://sim.ai/api/workflows/{workflow-id}/execute \ +curl -N -X POST https://sim.ai/api/v2/workflows/{workflow-id}/execute \ -H "Content-Type: application/json" \ -H "x-api-key: $SIM_API_KEY" \ -H "X-Sim-Stream-Protocol: agent-events-v1" \ -d '{ - "input": "Research this topic", + "input": { "prompt": "Research this topic" }, "stream": true, "selectedOutputs": ["agent_1.content"], "includeThinking": true, @@ -280,58 +280,60 @@ The `version` field is part of the external API contract. Treat the reference as ### Asynchronous -For long-running workflows, async mode returns an execution ID immediately so you don't need to hold the connection open. Add the `X-Execution-Mode: async` header to your request. The API returns HTTP 202 with an execution ID and status URL. Poll the execution resource until the run completes. +For long-running workflows, async mode returns an execution ID immediately so you don't need to hold the connection open. Set `"async": true` in the v2 request body. The API returns HTTP 202 with an execution ID and v2 status URL. Poll that execution resource until the run completes. ```bash -curl -X POST https://sim.ai/api/workflows/{workflow-id}/execute \ +curl -X POST https://sim.ai/api/v2/workflows/{workflow-id}/execute \ -H "Content-Type: application/json" \ -H "x-api-key: $SIM_API_KEY" \ - -H "X-Execution-Mode: async" \ - -d '{ "input": "Process this large dataset" }' + -d '{ "input": { "task": "Process this large dataset" }, "async": true }' ``` **Response** (HTTP 202): ```json { - "success": true, - "async": true, - "executionId": "c7a92e15-3f4b-4d8c-a1e6-9b0d5f2c8e74", - "message": "Workflow execution queued", - "statusUrl": "https://sim.ai/api/workflows/{workflow-id}/executions/c7a92e15-3f4b-4d8c-a1e6-9b0d5f2c8e74" + "data": { + "executionId": "c7a92e15-3f4b-4d8c-a1e6-9b0d5f2c8e74", + "statusUrl": "https://sim.ai/api/v2/workflows/{workflow-id}/executions/c7a92e15-3f4b-4d8c-a1e6-9b0d5f2c8e74" + } } ``` ```bash -curl "https://sim.ai/api/workflows/{workflow-id}/executions/{executionId}?includeOutput=true" \ +curl "https://sim.ai/api/v2/workflows/{workflow-id}/executions/{executionId}?includeOutput=true" \ -H "x-api-key: $SIM_API_KEY" ``` **While processing:** ```json { - "executionId": "c7a92e15-3f4b-4d8c-a1e6-9b0d5f2c8e74", - "workflowId": "{workflow-id}", - "status": "running", - "startedAt": "2025-09-10T12:00:01.000Z", - "endedAt": null, - "totalDurationMs": null, - "finalOutput": null + "data": { + "executionId": "c7a92e15-3f4b-4d8c-a1e6-9b0d5f2c8e74", + "workflowId": "{workflow-id}", + "status": "running", + "startedAt": "2025-09-10T12:00:01.000Z", + "endedAt": null, + "durationMs": null, + "output": null + } } ``` **When completed:** ```json { - "executionId": "c7a92e15-3f4b-4d8c-a1e6-9b0d5f2c8e74", - "workflowId": "{workflow-id}", - "status": "completed", - "startedAt": "2025-09-10T12:00:01.000Z", - "endedAt": "2025-09-10T12:00:05.000Z", - "totalDurationMs": 4000, - "finalOutput": { "result": "..." } + "data": { + "executionId": "c7a92e15-3f4b-4d8c-a1e6-9b0d5f2c8e74", + "workflowId": "{workflow-id}", + "status": "completed", + "startedAt": "2025-09-10T12:00:01.000Z", + "endedAt": "2025-09-10T12:00:05.000Z", + "durationMs": 4000, + "output": { "result": "..." } + } } ``` @@ -345,7 +347,7 @@ curl "https://sim.ai/api/workflows/{workflow-id}/executions/{executionId}?includ | `pending` | The durable execution record exists but has not started | | `running` | Workflow is actively executing | | `paused` | Workflow is waiting for a resume condition or input | -| `completed` | Finished successfully — `finalOutput` is populated when requested | +| `completed` | Finished successfully — `output` is populated when requested | | `failed` | Execution failed — `error` field contains the message | | `cancelled` | Execution was cancelled | diff --git a/apps/docs/content/docs/es/api-reference/getting-started.mdx b/apps/docs/content/docs/es/api-reference/getting-started.mdx index e593d678ff9..c8093e72c14 100644 --- a/apps/docs/content/docs/es/api-reference/getting-started.mdx +++ b/apps/docs/content/docs/es/api-reference/getting-started.mdx @@ -109,27 +109,27 @@ curl -X POST https://www.sim.ai/api/workflows/{workflowId}/execute \ -d '{"inputs": {}, "async": true}' ``` -This returns immediately with an `executionId` and `statusUrl`: +This returns immediately with a `jobId` and `statusUrl`: ```json { "success": true, - "executionId": "c7a92e15-3f4b-4d8c-a1e6-9b0d5f2c8e74", - "statusUrl": "https://www.sim.ai/api/workflows/{workflowId}/executions/c7a92e15-3f4b-4d8c-a1e6-9b0d5f2c8e74", - "message": "Workflow execution queued", + "jobId": "job_abc123", + "statusUrl": "https://www.sim.ai/api/jobs/job_abc123", + "message": "Workflow execution started", "async": true } ``` -Poll the [Get Execution Status](/api-reference/execution/getWorkflowExecution) endpoint until the status is terminal: +Poll the [Get Job Status](/api-reference/execution/getJobStatus) endpoint until the status is `completed` or `failed`: ```bash -curl https://www.sim.ai/api/workflows/{workflowId}/executions/{executionId}?includeOutput=true \ +curl https://www.sim.ai/api/jobs/{jobId} \ -H "X-API-Key: YOUR_API_KEY" ``` - Execution status transitions follow: `queued` → `running` → `completed`, `failed`, `cancelled`, or `paused`. The `finalOutput` field is populated for completed executions when `includeOutput=true`. + Job status transitions follow: `queued` → `processing` → `completed` or `failed`. The `output` field is only present when status is `completed`. ## Response Format diff --git a/apps/docs/content/docs/es/api-reference/python.mdx b/apps/docs/content/docs/es/api-reference/python.mdx index d7f89b25e74..cff0a2468b9 100644 --- a/apps/docs/content/docs/es/api-reference/python.mdx +++ b/apps/docs/content/docs/es/api-reference/python.mdx @@ -112,34 +112,30 @@ if is_ready: **Devuelve:** `bool` -##### get_workflow_execution() +##### get_job_status() -Get the status and optional outputs of a workflow execution. +Obtener el estado de una ejecución de trabajo asíncrono. ```python -status = client.get_workflow_execution("workflow-id", "execution-id", include_output=True) -print("Status:", status["status"]) # 'queued', 'running', 'completed', 'failed' +status = client.get_job_status("task-id-from-async-execution") +print("Status:", status["status"]) # 'queued', 'processing', 'completed', 'failed' if status["status"] == "completed": - print("Output:", status["finalOutput"]) + print("Output:", status["output"]) ``` -**Parameters:** -- `workflow_id` (str): The workflow ID -- `execution_id` (str): The execution ID returned from async execution -- `include_output` (bool, optional): Include the final output for completed executions -- `selected_outputs` (list[str], optional): Block output selectors to include +**Parámetros:** +- `task_id` (str): El ID de tarea devuelto de la ejecución asíncrona -**Returns:** `Dict[str, Any]` +**Devuelve:** `Dict[str, Any]` -**Response fields:** -- `executionId` (str): The execution ID -- `workflowId` (str): The workflow ID -- `status` (str): One of `'queued'`, `'pending'`, `'running'`, `'paused'`, `'completed'`, `'failed'`, `'cancelled'` -- `startedAt` / `endedAt` (str): Execution timestamps -- `totalDurationMs` (int, optional): Duration in milliseconds -- `finalOutput` (any, optional): The workflow output when requested for a completed execution -- `blockOutputs` (dict, optional): Requested block outputs -- `error` (str, optional): Failure details +**Campos de respuesta:** +- `success` (bool): Si la solicitud fue exitosa +- `taskId` (str): El ID de la tarea +- `status` (str): Uno de `'queued'`, `'processing'`, `'completed'`, `'failed'`, `'cancelled'` +- `metadata` (dict): Contiene `startedAt`, `completedAt`, y `duration` +- `output` (any, opcional): La salida del flujo de trabajo (cuando se completa) +- `error` (any, opcional): Detalles del error (cuando falla) +- `estimatedDuration` (int, opcional): Duración estimada en milisegundos (cuando está procesando/en cola) ##### execute_with_retry() @@ -275,10 +271,10 @@ class WorkflowExecutionResult: @dataclass class AsyncExecutionResult: success: bool - execution_id: str - status_url: str - message: str = "" - async_execution: bool = True + task_id: str + status: str # 'queued' + created_at: str + links: Dict[str, str] # e.g., {"status": "/api/jobs/{taskId}"} ``` ### WorkflowStatus @@ -493,31 +489,27 @@ def execute_async(): # Start async execution result = client.execute_workflow( "workflow-id", - input={"data": "large dataset"}, + input_data={"data": "large dataset"}, async_execution=True # Execute asynchronously ) # Check if result is an async execution - if hasattr(result, 'async_execution') and result.async_execution: - print(f"Execution ID: {result.execution_id}") - print(f"Status endpoint: {result.status_url}") + if hasattr(result, 'task_id'): + print(f"Task ID: {result.task_id}") + print(f"Status endpoint: {result.links['status']}") # Poll for completion - status = client.get_workflow_execution( - "workflow-id", result.execution_id, include_output=True - ) + status = client.get_job_status(result.task_id) - while status["status"] in ["queued", "pending", "running"]: + while status["status"] in ["queued", "processing"]: print(f"Current status: {status['status']}") time.sleep(2) # Wait 2 seconds - status = client.get_workflow_execution( - "workflow-id", result.execution_id, include_output=True - ) + status = client.get_job_status(result.task_id) if status["status"] == "completed": print("Workflow completed!") - print(f"Output: {status['finalOutput']}") - print(f"Duration: {status['totalDurationMs']}") + print(f"Output: {status['output']}") + print(f"Duration: {status['metadata']['duration']}") else: print(f"Workflow failed: {status['error']}") diff --git a/apps/docs/content/docs/es/api-reference/typescript.mdx b/apps/docs/content/docs/es/api-reference/typescript.mdx index 40fb802636d..58c3578c219 100644 --- a/apps/docs/content/docs/es/api-reference/typescript.mdx +++ b/apps/docs/content/docs/es/api-reference/typescript.mdx @@ -133,37 +133,31 @@ if (isReady) { **Devuelve:** `Promise` -##### getWorkflowExecution() +##### getJobStatus() -Get the status and optional outputs of a workflow execution. +Obtener el estado de una ejecución de trabajo asíncrono. ```typescript -const status = await client.getWorkflowExecution('workflow-id', 'execution-id', { - includeOutput: true -}); -console.log('Status:', status.status); // 'queued', 'running', 'completed', 'failed' +const status = await client.getJobStatus('task-id-from-async-execution'); +console.log('Status:', status.status); // 'queued', 'processing', 'completed', 'failed' if (status.status === 'completed') { - console.log('Output:', status.finalOutput); + console.log('Output:', status.output); } ``` -**Parameters:** -- `workflowId` (string): The workflow ID -- `executionId` (string): The execution ID returned from async execution -- `options.includeOutput` (boolean, optional): Include the final output for completed executions -- `options.selectedOutputs` (string[], optional): Block output selectors to include +**Parámetros:** +- `taskId` (string): El ID de tarea devuelto por la ejecución asíncrona -**Returns:** `Promise` +**Devuelve:** `Promise` -**Response fields:** -- `executionId` (string): The execution ID -- `workflowId` (string): The workflow ID -- `status` (string): One of `'queued'`, `'pending'`, `'running'`, `'paused'`, `'completed'`, `'failed'`, `'cancelled'` -- `startedAt` / `endedAt` (string): Execution timestamps -- `totalDurationMs` (number, nullable): Duration in milliseconds -- `finalOutput` (any, nullable): The workflow output when requested for a completed execution -- `blockOutputs` (object, nullable): Requested block outputs -- `error` (string, nullable): Failure details +**Campos de respuesta:** +- `success` (boolean): Si la solicitud fue exitosa +- `taskId` (string): El ID de la tarea +- `status` (string): Uno de `'queued'`, `'processing'`, `'completed'`, `'failed'`, `'cancelled'` +- `metadata` (object): Contiene `startedAt`, `completedAt`, y `duration` +- `output` (any, opcional): La salida del flujo de trabajo (cuando se completa) +- `error` (any, opcional): Detalles del error (cuando falla) +- `estimatedDuration` (number, opcional): Duración estimada en milisegundos (cuando está procesando/en cola) ##### executeWithRetry() @@ -292,10 +286,12 @@ interface WorkflowExecutionResult { ```typescript interface AsyncExecutionResult { success: boolean; - executionId: string; - statusUrl: string; - message: string; - async: true; + taskId: string; + status: 'queued'; + createdAt: string; + links: { + status: string; // e.g., "/api/jobs/{taskId}" + }; } ``` @@ -795,32 +791,29 @@ const client = new SimStudioClient({ async function executeAsync() { try { // Start async execution - const result = await client.executeWorkflow('workflow-id', { data: 'large dataset' }, { + const result = await client.executeWorkflow('workflow-id', { + input: { data: 'large dataset' }, async: true // Execute asynchronously }); // Check if result is an async execution - if ('async' in result && result.async) { - console.log('Execution ID:', result.executionId); - console.log('Status endpoint:', result.statusUrl); + if ('taskId' in result) { + console.log('Task ID:', result.taskId); + console.log('Status endpoint:', result.links.status); // Poll for completion - let status = await client.getWorkflowExecution('workflow-id', result.executionId, { - includeOutput: true - }); + let status = await client.getJobStatus(result.taskId); - while (status.status === 'queued' || status.status === 'pending' || status.status === 'running') { + while (status.status === 'queued' || status.status === 'processing') { console.log('Current status:', status.status); await new Promise(resolve => setTimeout(resolve, 2000)); // Wait 2 seconds - status = await client.getWorkflowExecution('workflow-id', result.executionId, { - includeOutput: true - }); + status = await client.getJobStatus(result.taskId); } if (status.status === 'completed') { console.log('Workflow completed!'); - console.log('Output:', status.finalOutput); - console.log('Duration:', status.totalDurationMs); + console.log('Output:', status.output); + console.log('Duration:', status.metadata.duration); } else { console.error('Workflow failed:', status.error); } diff --git a/apps/docs/content/docs/es/sdks/python.mdx b/apps/docs/content/docs/es/sdks/python.mdx index d7f89b25e74..cff0a2468b9 100644 --- a/apps/docs/content/docs/es/sdks/python.mdx +++ b/apps/docs/content/docs/es/sdks/python.mdx @@ -112,34 +112,30 @@ if is_ready: **Devuelve:** `bool` -##### get_workflow_execution() +##### get_job_status() -Get the status and optional outputs of a workflow execution. +Obtener el estado de una ejecución de trabajo asíncrono. ```python -status = client.get_workflow_execution("workflow-id", "execution-id", include_output=True) -print("Status:", status["status"]) # 'queued', 'running', 'completed', 'failed' +status = client.get_job_status("task-id-from-async-execution") +print("Status:", status["status"]) # 'queued', 'processing', 'completed', 'failed' if status["status"] == "completed": - print("Output:", status["finalOutput"]) + print("Output:", status["output"]) ``` -**Parameters:** -- `workflow_id` (str): The workflow ID -- `execution_id` (str): The execution ID returned from async execution -- `include_output` (bool, optional): Include the final output for completed executions -- `selected_outputs` (list[str], optional): Block output selectors to include +**Parámetros:** +- `task_id` (str): El ID de tarea devuelto de la ejecución asíncrona -**Returns:** `Dict[str, Any]` +**Devuelve:** `Dict[str, Any]` -**Response fields:** -- `executionId` (str): The execution ID -- `workflowId` (str): The workflow ID -- `status` (str): One of `'queued'`, `'pending'`, `'running'`, `'paused'`, `'completed'`, `'failed'`, `'cancelled'` -- `startedAt` / `endedAt` (str): Execution timestamps -- `totalDurationMs` (int, optional): Duration in milliseconds -- `finalOutput` (any, optional): The workflow output when requested for a completed execution -- `blockOutputs` (dict, optional): Requested block outputs -- `error` (str, optional): Failure details +**Campos de respuesta:** +- `success` (bool): Si la solicitud fue exitosa +- `taskId` (str): El ID de la tarea +- `status` (str): Uno de `'queued'`, `'processing'`, `'completed'`, `'failed'`, `'cancelled'` +- `metadata` (dict): Contiene `startedAt`, `completedAt`, y `duration` +- `output` (any, opcional): La salida del flujo de trabajo (cuando se completa) +- `error` (any, opcional): Detalles del error (cuando falla) +- `estimatedDuration` (int, opcional): Duración estimada en milisegundos (cuando está procesando/en cola) ##### execute_with_retry() @@ -275,10 +271,10 @@ class WorkflowExecutionResult: @dataclass class AsyncExecutionResult: success: bool - execution_id: str - status_url: str - message: str = "" - async_execution: bool = True + task_id: str + status: str # 'queued' + created_at: str + links: Dict[str, str] # e.g., {"status": "/api/jobs/{taskId}"} ``` ### WorkflowStatus @@ -493,31 +489,27 @@ def execute_async(): # Start async execution result = client.execute_workflow( "workflow-id", - input={"data": "large dataset"}, + input_data={"data": "large dataset"}, async_execution=True # Execute asynchronously ) # Check if result is an async execution - if hasattr(result, 'async_execution') and result.async_execution: - print(f"Execution ID: {result.execution_id}") - print(f"Status endpoint: {result.status_url}") + if hasattr(result, 'task_id'): + print(f"Task ID: {result.task_id}") + print(f"Status endpoint: {result.links['status']}") # Poll for completion - status = client.get_workflow_execution( - "workflow-id", result.execution_id, include_output=True - ) + status = client.get_job_status(result.task_id) - while status["status"] in ["queued", "pending", "running"]: + while status["status"] in ["queued", "processing"]: print(f"Current status: {status['status']}") time.sleep(2) # Wait 2 seconds - status = client.get_workflow_execution( - "workflow-id", result.execution_id, include_output=True - ) + status = client.get_job_status(result.task_id) if status["status"] == "completed": print("Workflow completed!") - print(f"Output: {status['finalOutput']}") - print(f"Duration: {status['totalDurationMs']}") + print(f"Output: {status['output']}") + print(f"Duration: {status['metadata']['duration']}") else: print(f"Workflow failed: {status['error']}") diff --git a/apps/docs/content/docs/es/sdks/typescript.mdx b/apps/docs/content/docs/es/sdks/typescript.mdx index 40fb802636d..58c3578c219 100644 --- a/apps/docs/content/docs/es/sdks/typescript.mdx +++ b/apps/docs/content/docs/es/sdks/typescript.mdx @@ -133,37 +133,31 @@ if (isReady) { **Devuelve:** `Promise` -##### getWorkflowExecution() +##### getJobStatus() -Get the status and optional outputs of a workflow execution. +Obtener el estado de una ejecución de trabajo asíncrono. ```typescript -const status = await client.getWorkflowExecution('workflow-id', 'execution-id', { - includeOutput: true -}); -console.log('Status:', status.status); // 'queued', 'running', 'completed', 'failed' +const status = await client.getJobStatus('task-id-from-async-execution'); +console.log('Status:', status.status); // 'queued', 'processing', 'completed', 'failed' if (status.status === 'completed') { - console.log('Output:', status.finalOutput); + console.log('Output:', status.output); } ``` -**Parameters:** -- `workflowId` (string): The workflow ID -- `executionId` (string): The execution ID returned from async execution -- `options.includeOutput` (boolean, optional): Include the final output for completed executions -- `options.selectedOutputs` (string[], optional): Block output selectors to include +**Parámetros:** +- `taskId` (string): El ID de tarea devuelto por la ejecución asíncrona -**Returns:** `Promise` +**Devuelve:** `Promise` -**Response fields:** -- `executionId` (string): The execution ID -- `workflowId` (string): The workflow ID -- `status` (string): One of `'queued'`, `'pending'`, `'running'`, `'paused'`, `'completed'`, `'failed'`, `'cancelled'` -- `startedAt` / `endedAt` (string): Execution timestamps -- `totalDurationMs` (number, nullable): Duration in milliseconds -- `finalOutput` (any, nullable): The workflow output when requested for a completed execution -- `blockOutputs` (object, nullable): Requested block outputs -- `error` (string, nullable): Failure details +**Campos de respuesta:** +- `success` (boolean): Si la solicitud fue exitosa +- `taskId` (string): El ID de la tarea +- `status` (string): Uno de `'queued'`, `'processing'`, `'completed'`, `'failed'`, `'cancelled'` +- `metadata` (object): Contiene `startedAt`, `completedAt`, y `duration` +- `output` (any, opcional): La salida del flujo de trabajo (cuando se completa) +- `error` (any, opcional): Detalles del error (cuando falla) +- `estimatedDuration` (number, opcional): Duración estimada en milisegundos (cuando está procesando/en cola) ##### executeWithRetry() @@ -292,10 +286,12 @@ interface WorkflowExecutionResult { ```typescript interface AsyncExecutionResult { success: boolean; - executionId: string; - statusUrl: string; - message: string; - async: true; + taskId: string; + status: 'queued'; + createdAt: string; + links: { + status: string; // e.g., "/api/jobs/{taskId}" + }; } ``` @@ -795,32 +791,29 @@ const client = new SimStudioClient({ async function executeAsync() { try { // Start async execution - const result = await client.executeWorkflow('workflow-id', { data: 'large dataset' }, { + const result = await client.executeWorkflow('workflow-id', { + input: { data: 'large dataset' }, async: true // Execute asynchronously }); // Check if result is an async execution - if ('async' in result && result.async) { - console.log('Execution ID:', result.executionId); - console.log('Status endpoint:', result.statusUrl); + if ('taskId' in result) { + console.log('Task ID:', result.taskId); + console.log('Status endpoint:', result.links.status); // Poll for completion - let status = await client.getWorkflowExecution('workflow-id', result.executionId, { - includeOutput: true - }); + let status = await client.getJobStatus(result.taskId); - while (status.status === 'queued' || status.status === 'pending' || status.status === 'running') { + while (status.status === 'queued' || status.status === 'processing') { console.log('Current status:', status.status); await new Promise(resolve => setTimeout(resolve, 2000)); // Wait 2 seconds - status = await client.getWorkflowExecution('workflow-id', result.executionId, { - includeOutput: true - }); + status = await client.getJobStatus(result.taskId); } if (status.status === 'completed') { console.log('Workflow completed!'); - console.log('Output:', status.finalOutput); - console.log('Duration:', status.totalDurationMs); + console.log('Output:', status.output); + console.log('Duration:', status.metadata.duration); } else { console.error('Workflow failed:', status.error); } diff --git a/apps/docs/content/docs/fr/api-reference/getting-started.mdx b/apps/docs/content/docs/fr/api-reference/getting-started.mdx index e593d678ff9..c8093e72c14 100644 --- a/apps/docs/content/docs/fr/api-reference/getting-started.mdx +++ b/apps/docs/content/docs/fr/api-reference/getting-started.mdx @@ -109,27 +109,27 @@ curl -X POST https://www.sim.ai/api/workflows/{workflowId}/execute \ -d '{"inputs": {}, "async": true}' ``` -This returns immediately with an `executionId` and `statusUrl`: +This returns immediately with a `jobId` and `statusUrl`: ```json { "success": true, - "executionId": "c7a92e15-3f4b-4d8c-a1e6-9b0d5f2c8e74", - "statusUrl": "https://www.sim.ai/api/workflows/{workflowId}/executions/c7a92e15-3f4b-4d8c-a1e6-9b0d5f2c8e74", - "message": "Workflow execution queued", + "jobId": "job_abc123", + "statusUrl": "https://www.sim.ai/api/jobs/job_abc123", + "message": "Workflow execution started", "async": true } ``` -Poll the [Get Execution Status](/api-reference/execution/getWorkflowExecution) endpoint until the status is terminal: +Poll the [Get Job Status](/api-reference/execution/getJobStatus) endpoint until the status is `completed` or `failed`: ```bash -curl https://www.sim.ai/api/workflows/{workflowId}/executions/{executionId}?includeOutput=true \ +curl https://www.sim.ai/api/jobs/{jobId} \ -H "X-API-Key: YOUR_API_KEY" ``` - Execution status transitions follow: `queued` → `running` → `completed`, `failed`, `cancelled`, or `paused`. The `finalOutput` field is populated for completed executions when `includeOutput=true`. + Job status transitions follow: `queued` → `processing` → `completed` or `failed`. The `output` field is only present when status is `completed`. ## Response Format diff --git a/apps/docs/content/docs/fr/api-reference/python.mdx b/apps/docs/content/docs/fr/api-reference/python.mdx index 797b759f276..268bc7657cf 100644 --- a/apps/docs/content/docs/fr/api-reference/python.mdx +++ b/apps/docs/content/docs/fr/api-reference/python.mdx @@ -112,34 +112,30 @@ if is_ready: **Retourne :** `bool` -##### get_workflow_execution() +##### get_job_status() -Get the status and optional outputs of a workflow execution. +Obtenir le statut d'une exécution de tâche asynchrone. ```python -status = client.get_workflow_execution("workflow-id", "execution-id", include_output=True) -print("Status:", status["status"]) # 'queued', 'running', 'completed', 'failed' +status = client.get_job_status("task-id-from-async-execution") +print("Status:", status["status"]) # 'queued', 'processing', 'completed', 'failed' if status["status"] == "completed": - print("Output:", status["finalOutput"]) + print("Output:", status["output"]) ``` -**Parameters:** -- `workflow_id` (str): The workflow ID -- `execution_id` (str): The execution ID returned from async execution -- `include_output` (bool, optional): Include the final output for completed executions -- `selected_outputs` (list[str], optional): Block output selectors to include +**Paramètres :** +- `task_id` (str) : L'identifiant de tâche retourné par l'exécution asynchrone -**Returns:** `Dict[str, Any]` +**Retourne :** `Dict[str, Any]` -**Response fields:** -- `executionId` (str): The execution ID -- `workflowId` (str): The workflow ID -- `status` (str): One of `'queued'`, `'pending'`, `'running'`, `'paused'`, `'completed'`, `'failed'`, `'cancelled'` -- `startedAt` / `endedAt` (str): Execution timestamps -- `totalDurationMs` (int, optional): Duration in milliseconds -- `finalOutput` (any, optional): The workflow output when requested for a completed execution -- `blockOutputs` (dict, optional): Requested block outputs -- `error` (str, optional): Failure details +**Champs de réponse :** +- `success` (bool) : Si la requête a réussi +- `taskId` (str) : L'identifiant de la tâche +- `status` (str) : L'un des états suivants : `'queued'`, `'processing'`, `'completed'`, `'failed'`, `'cancelled'` +- `metadata` (dict) : Contient `startedAt`, `completedAt`, et `duration` +- `output` (any, facultatif) : La sortie du workflow (une fois terminé) +- `error` (any, facultatif) : Détails de l'erreur (en cas d'échec) +- `estimatedDuration` (int, facultatif) : Durée estimée en millisecondes (lors du traitement/mise en file d'attente) ##### execute_with_retry() @@ -275,10 +271,10 @@ class WorkflowExecutionResult: @dataclass class AsyncExecutionResult: success: bool - execution_id: str - status_url: str - message: str = "" - async_execution: bool = True + task_id: str + status: str # 'queued' + created_at: str + links: Dict[str, str] # e.g., {"status": "/api/jobs/{taskId}"} ``` ### WorkflowStatus @@ -493,31 +489,27 @@ def execute_async(): # Start async execution result = client.execute_workflow( "workflow-id", - input={"data": "large dataset"}, + input_data={"data": "large dataset"}, async_execution=True # Execute asynchronously ) # Check if result is an async execution - if hasattr(result, 'async_execution') and result.async_execution: - print(f"Execution ID: {result.execution_id}") - print(f"Status endpoint: {result.status_url}") + if hasattr(result, 'task_id'): + print(f"Task ID: {result.task_id}") + print(f"Status endpoint: {result.links['status']}") # Poll for completion - status = client.get_workflow_execution( - "workflow-id", result.execution_id, include_output=True - ) + status = client.get_job_status(result.task_id) - while status["status"] in ["queued", "pending", "running"]: + while status["status"] in ["queued", "processing"]: print(f"Current status: {status['status']}") time.sleep(2) # Wait 2 seconds - status = client.get_workflow_execution( - "workflow-id", result.execution_id, include_output=True - ) + status = client.get_job_status(result.task_id) if status["status"] == "completed": print("Workflow completed!") - print(f"Output: {status['finalOutput']}") - print(f"Duration: {status['totalDurationMs']}") + print(f"Output: {status['output']}") + print(f"Duration: {status['metadata']['duration']}") else: print(f"Workflow failed: {status['error']}") diff --git a/apps/docs/content/docs/fr/api-reference/typescript.mdx b/apps/docs/content/docs/fr/api-reference/typescript.mdx index 4c23794f457..0c6e98781af 100644 --- a/apps/docs/content/docs/fr/api-reference/typescript.mdx +++ b/apps/docs/content/docs/fr/api-reference/typescript.mdx @@ -133,37 +133,31 @@ if (isReady) { **Retourne :** `Promise` -##### getWorkflowExecution() +##### getJobStatus() -Get the status and optional outputs of a workflow execution. +Obtenir le statut d'une exécution de tâche asynchrone. ```typescript -const status = await client.getWorkflowExecution('workflow-id', 'execution-id', { - includeOutput: true -}); -console.log('Status:', status.status); // 'queued', 'running', 'completed', 'failed' +const status = await client.getJobStatus('task-id-from-async-execution'); +console.log('Status:', status.status); // 'queued', 'processing', 'completed', 'failed' if (status.status === 'completed') { - console.log('Output:', status.finalOutput); + console.log('Output:', status.output); } ``` -**Parameters:** -- `workflowId` (string): The workflow ID -- `executionId` (string): The execution ID returned from async execution -- `options.includeOutput` (boolean, optional): Include the final output for completed executions -- `options.selectedOutputs` (string[], optional): Block output selectors to include +**Paramètres :** +- `taskId` (string) : L'identifiant de tâche retourné par l'exécution asynchrone -**Returns:** `Promise` +**Retourne :** `Promise` -**Response fields:** -- `executionId` (string): The execution ID -- `workflowId` (string): The workflow ID -- `status` (string): One of `'queued'`, `'pending'`, `'running'`, `'paused'`, `'completed'`, `'failed'`, `'cancelled'` -- `startedAt` / `endedAt` (string): Execution timestamps -- `totalDurationMs` (number, nullable): Duration in milliseconds -- `finalOutput` (any, nullable): The workflow output when requested for a completed execution -- `blockOutputs` (object, nullable): Requested block outputs -- `error` (string, nullable): Failure details +**Champs de réponse :** +- `success` (boolean) : Indique si la requête a réussi +- `taskId` (string) : L'identifiant de la tâche +- `status` (string) : L'un des états suivants : `'queued'`, `'processing'`, `'completed'`, `'failed'`, `'cancelled'` +- `metadata` (object) : Contient `startedAt`, `completedAt` et `duration` +- `output` (any, facultatif) : La sortie du workflow (une fois terminé) +- `error` (any, facultatif) : Détails de l'erreur (en cas d'échec) +- `estimatedDuration` (number, facultatif) : Durée estimée en millisecondes (lorsqu'en traitement/en file d'attente) ##### executeWithRetry() @@ -292,10 +286,12 @@ interface WorkflowExecutionResult { ```typescript interface AsyncExecutionResult { success: boolean; - executionId: string; - statusUrl: string; - message: string; - async: true; + taskId: string; + status: 'queued'; + createdAt: string; + links: { + status: string; // e.g., "/api/jobs/{taskId}" + }; } ``` @@ -795,32 +791,29 @@ const client = new SimStudioClient({ async function executeAsync() { try { // Start async execution - const result = await client.executeWorkflow('workflow-id', { data: 'large dataset' }, { + const result = await client.executeWorkflow('workflow-id', { + input: { data: 'large dataset' }, async: true // Execute asynchronously }); // Check if result is an async execution - if ('async' in result && result.async) { - console.log('Execution ID:', result.executionId); - console.log('Status endpoint:', result.statusUrl); + if ('taskId' in result) { + console.log('Task ID:', result.taskId); + console.log('Status endpoint:', result.links.status); // Poll for completion - let status = await client.getWorkflowExecution('workflow-id', result.executionId, { - includeOutput: true - }); + let status = await client.getJobStatus(result.taskId); - while (status.status === 'queued' || status.status === 'pending' || status.status === 'running') { + while (status.status === 'queued' || status.status === 'processing') { console.log('Current status:', status.status); await new Promise(resolve => setTimeout(resolve, 2000)); // Wait 2 seconds - status = await client.getWorkflowExecution('workflow-id', result.executionId, { - includeOutput: true - }); + status = await client.getJobStatus(result.taskId); } if (status.status === 'completed') { console.log('Workflow completed!'); - console.log('Output:', status.finalOutput); - console.log('Duration:', status.totalDurationMs); + console.log('Output:', status.output); + console.log('Duration:', status.metadata.duration); } else { console.error('Workflow failed:', status.error); } diff --git a/apps/docs/content/docs/fr/sdks/python.mdx b/apps/docs/content/docs/fr/sdks/python.mdx index 797b759f276..268bc7657cf 100644 --- a/apps/docs/content/docs/fr/sdks/python.mdx +++ b/apps/docs/content/docs/fr/sdks/python.mdx @@ -112,34 +112,30 @@ if is_ready: **Retourne :** `bool` -##### get_workflow_execution() +##### get_job_status() -Get the status and optional outputs of a workflow execution. +Obtenir le statut d'une exécution de tâche asynchrone. ```python -status = client.get_workflow_execution("workflow-id", "execution-id", include_output=True) -print("Status:", status["status"]) # 'queued', 'running', 'completed', 'failed' +status = client.get_job_status("task-id-from-async-execution") +print("Status:", status["status"]) # 'queued', 'processing', 'completed', 'failed' if status["status"] == "completed": - print("Output:", status["finalOutput"]) + print("Output:", status["output"]) ``` -**Parameters:** -- `workflow_id` (str): The workflow ID -- `execution_id` (str): The execution ID returned from async execution -- `include_output` (bool, optional): Include the final output for completed executions -- `selected_outputs` (list[str], optional): Block output selectors to include +**Paramètres :** +- `task_id` (str) : L'identifiant de tâche retourné par l'exécution asynchrone -**Returns:** `Dict[str, Any]` +**Retourne :** `Dict[str, Any]` -**Response fields:** -- `executionId` (str): The execution ID -- `workflowId` (str): The workflow ID -- `status` (str): One of `'queued'`, `'pending'`, `'running'`, `'paused'`, `'completed'`, `'failed'`, `'cancelled'` -- `startedAt` / `endedAt` (str): Execution timestamps -- `totalDurationMs` (int, optional): Duration in milliseconds -- `finalOutput` (any, optional): The workflow output when requested for a completed execution -- `blockOutputs` (dict, optional): Requested block outputs -- `error` (str, optional): Failure details +**Champs de réponse :** +- `success` (bool) : Si la requête a réussi +- `taskId` (str) : L'identifiant de la tâche +- `status` (str) : L'un des états suivants : `'queued'`, `'processing'`, `'completed'`, `'failed'`, `'cancelled'` +- `metadata` (dict) : Contient `startedAt`, `completedAt`, et `duration` +- `output` (any, facultatif) : La sortie du workflow (une fois terminé) +- `error` (any, facultatif) : Détails de l'erreur (en cas d'échec) +- `estimatedDuration` (int, facultatif) : Durée estimée en millisecondes (lors du traitement/mise en file d'attente) ##### execute_with_retry() @@ -275,10 +271,10 @@ class WorkflowExecutionResult: @dataclass class AsyncExecutionResult: success: bool - execution_id: str - status_url: str - message: str = "" - async_execution: bool = True + task_id: str + status: str # 'queued' + created_at: str + links: Dict[str, str] # e.g., {"status": "/api/jobs/{taskId}"} ``` ### WorkflowStatus @@ -493,31 +489,27 @@ def execute_async(): # Start async execution result = client.execute_workflow( "workflow-id", - input={"data": "large dataset"}, + input_data={"data": "large dataset"}, async_execution=True # Execute asynchronously ) # Check if result is an async execution - if hasattr(result, 'async_execution') and result.async_execution: - print(f"Execution ID: {result.execution_id}") - print(f"Status endpoint: {result.status_url}") + if hasattr(result, 'task_id'): + print(f"Task ID: {result.task_id}") + print(f"Status endpoint: {result.links['status']}") # Poll for completion - status = client.get_workflow_execution( - "workflow-id", result.execution_id, include_output=True - ) + status = client.get_job_status(result.task_id) - while status["status"] in ["queued", "pending", "running"]: + while status["status"] in ["queued", "processing"]: print(f"Current status: {status['status']}") time.sleep(2) # Wait 2 seconds - status = client.get_workflow_execution( - "workflow-id", result.execution_id, include_output=True - ) + status = client.get_job_status(result.task_id) if status["status"] == "completed": print("Workflow completed!") - print(f"Output: {status['finalOutput']}") - print(f"Duration: {status['totalDurationMs']}") + print(f"Output: {status['output']}") + print(f"Duration: {status['metadata']['duration']}") else: print(f"Workflow failed: {status['error']}") diff --git a/apps/docs/content/docs/fr/sdks/typescript.mdx b/apps/docs/content/docs/fr/sdks/typescript.mdx index 4c23794f457..0c6e98781af 100644 --- a/apps/docs/content/docs/fr/sdks/typescript.mdx +++ b/apps/docs/content/docs/fr/sdks/typescript.mdx @@ -133,37 +133,31 @@ if (isReady) { **Retourne :** `Promise` -##### getWorkflowExecution() +##### getJobStatus() -Get the status and optional outputs of a workflow execution. +Obtenir le statut d'une exécution de tâche asynchrone. ```typescript -const status = await client.getWorkflowExecution('workflow-id', 'execution-id', { - includeOutput: true -}); -console.log('Status:', status.status); // 'queued', 'running', 'completed', 'failed' +const status = await client.getJobStatus('task-id-from-async-execution'); +console.log('Status:', status.status); // 'queued', 'processing', 'completed', 'failed' if (status.status === 'completed') { - console.log('Output:', status.finalOutput); + console.log('Output:', status.output); } ``` -**Parameters:** -- `workflowId` (string): The workflow ID -- `executionId` (string): The execution ID returned from async execution -- `options.includeOutput` (boolean, optional): Include the final output for completed executions -- `options.selectedOutputs` (string[], optional): Block output selectors to include +**Paramètres :** +- `taskId` (string) : L'identifiant de tâche retourné par l'exécution asynchrone -**Returns:** `Promise` +**Retourne :** `Promise` -**Response fields:** -- `executionId` (string): The execution ID -- `workflowId` (string): The workflow ID -- `status` (string): One of `'queued'`, `'pending'`, `'running'`, `'paused'`, `'completed'`, `'failed'`, `'cancelled'` -- `startedAt` / `endedAt` (string): Execution timestamps -- `totalDurationMs` (number, nullable): Duration in milliseconds -- `finalOutput` (any, nullable): The workflow output when requested for a completed execution -- `blockOutputs` (object, nullable): Requested block outputs -- `error` (string, nullable): Failure details +**Champs de réponse :** +- `success` (boolean) : Indique si la requête a réussi +- `taskId` (string) : L'identifiant de la tâche +- `status` (string) : L'un des états suivants : `'queued'`, `'processing'`, `'completed'`, `'failed'`, `'cancelled'` +- `metadata` (object) : Contient `startedAt`, `completedAt` et `duration` +- `output` (any, facultatif) : La sortie du workflow (une fois terminé) +- `error` (any, facultatif) : Détails de l'erreur (en cas d'échec) +- `estimatedDuration` (number, facultatif) : Durée estimée en millisecondes (lorsqu'en traitement/en file d'attente) ##### executeWithRetry() @@ -292,10 +286,12 @@ interface WorkflowExecutionResult { ```typescript interface AsyncExecutionResult { success: boolean; - executionId: string; - statusUrl: string; - message: string; - async: true; + taskId: string; + status: 'queued'; + createdAt: string; + links: { + status: string; // e.g., "/api/jobs/{taskId}" + }; } ``` @@ -795,32 +791,29 @@ const client = new SimStudioClient({ async function executeAsync() { try { // Start async execution - const result = await client.executeWorkflow('workflow-id', { data: 'large dataset' }, { + const result = await client.executeWorkflow('workflow-id', { + input: { data: 'large dataset' }, async: true // Execute asynchronously }); // Check if result is an async execution - if ('async' in result && result.async) { - console.log('Execution ID:', result.executionId); - console.log('Status endpoint:', result.statusUrl); + if ('taskId' in result) { + console.log('Task ID:', result.taskId); + console.log('Status endpoint:', result.links.status); // Poll for completion - let status = await client.getWorkflowExecution('workflow-id', result.executionId, { - includeOutput: true - }); + let status = await client.getJobStatus(result.taskId); - while (status.status === 'queued' || status.status === 'pending' || status.status === 'running') { + while (status.status === 'queued' || status.status === 'processing') { console.log('Current status:', status.status); await new Promise(resolve => setTimeout(resolve, 2000)); // Wait 2 seconds - status = await client.getWorkflowExecution('workflow-id', result.executionId, { - includeOutput: true - }); + status = await client.getJobStatus(result.taskId); } if (status.status === 'completed') { console.log('Workflow completed!'); - console.log('Output:', status.finalOutput); - console.log('Duration:', status.totalDurationMs); + console.log('Output:', status.output); + console.log('Duration:', status.metadata.duration); } else { console.error('Workflow failed:', status.error); } diff --git a/apps/docs/content/docs/ja/api-reference/getting-started.mdx b/apps/docs/content/docs/ja/api-reference/getting-started.mdx index e593d678ff9..c8093e72c14 100644 --- a/apps/docs/content/docs/ja/api-reference/getting-started.mdx +++ b/apps/docs/content/docs/ja/api-reference/getting-started.mdx @@ -109,27 +109,27 @@ curl -X POST https://www.sim.ai/api/workflows/{workflowId}/execute \ -d '{"inputs": {}, "async": true}' ``` -This returns immediately with an `executionId` and `statusUrl`: +This returns immediately with a `jobId` and `statusUrl`: ```json { "success": true, - "executionId": "c7a92e15-3f4b-4d8c-a1e6-9b0d5f2c8e74", - "statusUrl": "https://www.sim.ai/api/workflows/{workflowId}/executions/c7a92e15-3f4b-4d8c-a1e6-9b0d5f2c8e74", - "message": "Workflow execution queued", + "jobId": "job_abc123", + "statusUrl": "https://www.sim.ai/api/jobs/job_abc123", + "message": "Workflow execution started", "async": true } ``` -Poll the [Get Execution Status](/api-reference/execution/getWorkflowExecution) endpoint until the status is terminal: +Poll the [Get Job Status](/api-reference/execution/getJobStatus) endpoint until the status is `completed` or `failed`: ```bash -curl https://www.sim.ai/api/workflows/{workflowId}/executions/{executionId}?includeOutput=true \ +curl https://www.sim.ai/api/jobs/{jobId} \ -H "X-API-Key: YOUR_API_KEY" ``` - Execution status transitions follow: `queued` → `running` → `completed`, `failed`, `cancelled`, or `paused`. The `finalOutput` field is populated for completed executions when `includeOutput=true`. + Job status transitions follow: `queued` → `processing` → `completed` or `failed`. The `output` field is only present when status is `completed`. ## Response Format diff --git a/apps/docs/content/docs/ja/api-reference/python.mdx b/apps/docs/content/docs/ja/api-reference/python.mdx index 14bb06c68b2..de4467f8a2a 100644 --- a/apps/docs/content/docs/ja/api-reference/python.mdx +++ b/apps/docs/content/docs/ja/api-reference/python.mdx @@ -112,34 +112,30 @@ if is_ready: **戻り値:** `bool` -##### get_workflow_execution() +##### get_job_status() -Get the status and optional outputs of a workflow execution. +非同期ジョブ実行のステータスを取得します。 ```python -status = client.get_workflow_execution("workflow-id", "execution-id", include_output=True) -print("Status:", status["status"]) # 'queued', 'running', 'completed', 'failed' +status = client.get_job_status("task-id-from-async-execution") +print("Status:", status["status"]) # 'queued', 'processing', 'completed', 'failed' if status["status"] == "completed": - print("Output:", status["finalOutput"]) + print("Output:", status["output"]) ``` -**Parameters:** -- `workflow_id` (str): The workflow ID -- `execution_id` (str): The execution ID returned from async execution -- `include_output` (bool, optional): Include the final output for completed executions -- `selected_outputs` (list[str], optional): Block output selectors to include +**パラメータ:** +- `task_id` (str): 非同期実行から返されたタスクID -**Returns:** `Dict[str, Any]` +**戻り値:** `Dict[str, Any]` -**Response fields:** -- `executionId` (str): The execution ID -- `workflowId` (str): The workflow ID -- `status` (str): One of `'queued'`, `'pending'`, `'running'`, `'paused'`, `'completed'`, `'failed'`, `'cancelled'` -- `startedAt` / `endedAt` (str): Execution timestamps -- `totalDurationMs` (int, optional): Duration in milliseconds -- `finalOutput` (any, optional): The workflow output when requested for a completed execution -- `blockOutputs` (dict, optional): Requested block outputs -- `error` (str, optional): Failure details +**レスポンスフィールド:** +- `success` (bool): リクエストが成功したかどうか +- `taskId` (str): タスクID +- `status` (str): 次のいずれか: `'queued'`, `'processing'`, `'completed'`, `'failed'`, `'cancelled'` +- `metadata` (dict): `startedAt`, `completedAt`, `duration`を含む +- `output` (any, オプション): ワークフロー出力(完了時) +- `error` (any, オプション): エラー詳細(失敗時) +- `estimatedDuration` (int, オプション): 推定所要時間(ミリ秒)(処理中/キュー時) ##### execute_with_retry() @@ -275,10 +271,10 @@ class WorkflowExecutionResult: @dataclass class AsyncExecutionResult: success: bool - execution_id: str - status_url: str - message: str = "" - async_execution: bool = True + task_id: str + status: str # 'queued' + created_at: str + links: Dict[str, str] # e.g., {"status": "/api/jobs/{taskId}"} ``` ### WorkflowStatus @@ -493,31 +489,27 @@ def execute_async(): # Start async execution result = client.execute_workflow( "workflow-id", - input={"data": "large dataset"}, + input_data={"data": "large dataset"}, async_execution=True # Execute asynchronously ) # Check if result is an async execution - if hasattr(result, 'async_execution') and result.async_execution: - print(f"Execution ID: {result.execution_id}") - print(f"Status endpoint: {result.status_url}") + if hasattr(result, 'task_id'): + print(f"Task ID: {result.task_id}") + print(f"Status endpoint: {result.links['status']}") # Poll for completion - status = client.get_workflow_execution( - "workflow-id", result.execution_id, include_output=True - ) + status = client.get_job_status(result.task_id) - while status["status"] in ["queued", "pending", "running"]: + while status["status"] in ["queued", "processing"]: print(f"Current status: {status['status']}") time.sleep(2) # Wait 2 seconds - status = client.get_workflow_execution( - "workflow-id", result.execution_id, include_output=True - ) + status = client.get_job_status(result.task_id) if status["status"] == "completed": print("Workflow completed!") - print(f"Output: {status['finalOutput']}") - print(f"Duration: {status['totalDurationMs']}") + print(f"Output: {status['output']}") + print(f"Duration: {status['metadata']['duration']}") else: print(f"Workflow failed: {status['error']}") diff --git a/apps/docs/content/docs/ja/api-reference/typescript.mdx b/apps/docs/content/docs/ja/api-reference/typescript.mdx index 6fd184ecd0f..a224c7663de 100644 --- a/apps/docs/content/docs/ja/api-reference/typescript.mdx +++ b/apps/docs/content/docs/ja/api-reference/typescript.mdx @@ -133,37 +133,31 @@ if (isReady) { **戻り値:** `Promise` -##### getWorkflowExecution() +##### getJobStatus() -Get the status and optional outputs of a workflow execution. +非同期ジョブ実行のステータスを取得します。 ```typescript -const status = await client.getWorkflowExecution('workflow-id', 'execution-id', { - includeOutput: true -}); -console.log('Status:', status.status); // 'queued', 'running', 'completed', 'failed' +const status = await client.getJobStatus('task-id-from-async-execution'); +console.log('Status:', status.status); // 'queued', 'processing', 'completed', 'failed' if (status.status === 'completed') { - console.log('Output:', status.finalOutput); + console.log('Output:', status.output); } ``` -**Parameters:** -- `workflowId` (string): The workflow ID -- `executionId` (string): The execution ID returned from async execution -- `options.includeOutput` (boolean, optional): Include the final output for completed executions -- `options.selectedOutputs` (string[], optional): Block output selectors to include +**パラメータ:** +- `taskId` (string): 非同期実行から返されたタスクID -**Returns:** `Promise` +**戻り値:** `Promise` -**Response fields:** -- `executionId` (string): The execution ID -- `workflowId` (string): The workflow ID -- `status` (string): One of `'queued'`, `'pending'`, `'running'`, `'paused'`, `'completed'`, `'failed'`, `'cancelled'` -- `startedAt` / `endedAt` (string): Execution timestamps -- `totalDurationMs` (number, nullable): Duration in milliseconds -- `finalOutput` (any, nullable): The workflow output when requested for a completed execution -- `blockOutputs` (object, nullable): Requested block outputs -- `error` (string, nullable): Failure details +**レスポンスフィールド:** +- `success` (boolean): リクエストが成功したかどうか +- `taskId` (string): タスクID +- `status` (string): 次のいずれか `'queued'`, `'processing'`, `'completed'`, `'failed'`, `'cancelled'` +- `metadata` (object): `startedAt`, `completedAt`, および `duration` を含む +- `output` (any, オプション): ワークフロー出力(完了時) +- `error` (any, オプション): エラー詳細(失敗時) +- `estimatedDuration` (number, オプション): 推定所要時間(ミリ秒)(処理中/キュー時) ##### executeWithRetry() @@ -292,10 +286,12 @@ interface WorkflowExecutionResult { ```typescript interface AsyncExecutionResult { success: boolean; - executionId: string; - statusUrl: string; - message: string; - async: true; + taskId: string; + status: 'queued'; + createdAt: string; + links: { + status: string; // e.g., "/api/jobs/{taskId}" + }; } ``` @@ -795,32 +791,29 @@ const client = new SimStudioClient({ async function executeAsync() { try { // Start async execution - const result = await client.executeWorkflow('workflow-id', { data: 'large dataset' }, { + const result = await client.executeWorkflow('workflow-id', { + input: { data: 'large dataset' }, async: true // Execute asynchronously }); // Check if result is an async execution - if ('async' in result && result.async) { - console.log('Execution ID:', result.executionId); - console.log('Status endpoint:', result.statusUrl); + if ('taskId' in result) { + console.log('Task ID:', result.taskId); + console.log('Status endpoint:', result.links.status); // Poll for completion - let status = await client.getWorkflowExecution('workflow-id', result.executionId, { - includeOutput: true - }); + let status = await client.getJobStatus(result.taskId); - while (status.status === 'queued' || status.status === 'pending' || status.status === 'running') { + while (status.status === 'queued' || status.status === 'processing') { console.log('Current status:', status.status); await new Promise(resolve => setTimeout(resolve, 2000)); // Wait 2 seconds - status = await client.getWorkflowExecution('workflow-id', result.executionId, { - includeOutput: true - }); + status = await client.getJobStatus(result.taskId); } if (status.status === 'completed') { console.log('Workflow completed!'); - console.log('Output:', status.finalOutput); - console.log('Duration:', status.totalDurationMs); + console.log('Output:', status.output); + console.log('Duration:', status.metadata.duration); } else { console.error('Workflow failed:', status.error); } diff --git a/apps/docs/content/docs/ja/sdks/python.mdx b/apps/docs/content/docs/ja/sdks/python.mdx index 14bb06c68b2..de4467f8a2a 100644 --- a/apps/docs/content/docs/ja/sdks/python.mdx +++ b/apps/docs/content/docs/ja/sdks/python.mdx @@ -112,34 +112,30 @@ if is_ready: **戻り値:** `bool` -##### get_workflow_execution() +##### get_job_status() -Get the status and optional outputs of a workflow execution. +非同期ジョブ実行のステータスを取得します。 ```python -status = client.get_workflow_execution("workflow-id", "execution-id", include_output=True) -print("Status:", status["status"]) # 'queued', 'running', 'completed', 'failed' +status = client.get_job_status("task-id-from-async-execution") +print("Status:", status["status"]) # 'queued', 'processing', 'completed', 'failed' if status["status"] == "completed": - print("Output:", status["finalOutput"]) + print("Output:", status["output"]) ``` -**Parameters:** -- `workflow_id` (str): The workflow ID -- `execution_id` (str): The execution ID returned from async execution -- `include_output` (bool, optional): Include the final output for completed executions -- `selected_outputs` (list[str], optional): Block output selectors to include +**パラメータ:** +- `task_id` (str): 非同期実行から返されたタスクID -**Returns:** `Dict[str, Any]` +**戻り値:** `Dict[str, Any]` -**Response fields:** -- `executionId` (str): The execution ID -- `workflowId` (str): The workflow ID -- `status` (str): One of `'queued'`, `'pending'`, `'running'`, `'paused'`, `'completed'`, `'failed'`, `'cancelled'` -- `startedAt` / `endedAt` (str): Execution timestamps -- `totalDurationMs` (int, optional): Duration in milliseconds -- `finalOutput` (any, optional): The workflow output when requested for a completed execution -- `blockOutputs` (dict, optional): Requested block outputs -- `error` (str, optional): Failure details +**レスポンスフィールド:** +- `success` (bool): リクエストが成功したかどうか +- `taskId` (str): タスクID +- `status` (str): 次のいずれか: `'queued'`, `'processing'`, `'completed'`, `'failed'`, `'cancelled'` +- `metadata` (dict): `startedAt`, `completedAt`, `duration`を含む +- `output` (any, オプション): ワークフロー出力(完了時) +- `error` (any, オプション): エラー詳細(失敗時) +- `estimatedDuration` (int, オプション): 推定所要時間(ミリ秒)(処理中/キュー時) ##### execute_with_retry() @@ -275,10 +271,10 @@ class WorkflowExecutionResult: @dataclass class AsyncExecutionResult: success: bool - execution_id: str - status_url: str - message: str = "" - async_execution: bool = True + task_id: str + status: str # 'queued' + created_at: str + links: Dict[str, str] # e.g., {"status": "/api/jobs/{taskId}"} ``` ### WorkflowStatus @@ -493,31 +489,27 @@ def execute_async(): # Start async execution result = client.execute_workflow( "workflow-id", - input={"data": "large dataset"}, + input_data={"data": "large dataset"}, async_execution=True # Execute asynchronously ) # Check if result is an async execution - if hasattr(result, 'async_execution') and result.async_execution: - print(f"Execution ID: {result.execution_id}") - print(f"Status endpoint: {result.status_url}") + if hasattr(result, 'task_id'): + print(f"Task ID: {result.task_id}") + print(f"Status endpoint: {result.links['status']}") # Poll for completion - status = client.get_workflow_execution( - "workflow-id", result.execution_id, include_output=True - ) + status = client.get_job_status(result.task_id) - while status["status"] in ["queued", "pending", "running"]: + while status["status"] in ["queued", "processing"]: print(f"Current status: {status['status']}") time.sleep(2) # Wait 2 seconds - status = client.get_workflow_execution( - "workflow-id", result.execution_id, include_output=True - ) + status = client.get_job_status(result.task_id) if status["status"] == "completed": print("Workflow completed!") - print(f"Output: {status['finalOutput']}") - print(f"Duration: {status['totalDurationMs']}") + print(f"Output: {status['output']}") + print(f"Duration: {status['metadata']['duration']}") else: print(f"Workflow failed: {status['error']}") diff --git a/apps/docs/content/docs/ja/sdks/typescript.mdx b/apps/docs/content/docs/ja/sdks/typescript.mdx index 6fd184ecd0f..a224c7663de 100644 --- a/apps/docs/content/docs/ja/sdks/typescript.mdx +++ b/apps/docs/content/docs/ja/sdks/typescript.mdx @@ -133,37 +133,31 @@ if (isReady) { **戻り値:** `Promise` -##### getWorkflowExecution() +##### getJobStatus() -Get the status and optional outputs of a workflow execution. +非同期ジョブ実行のステータスを取得します。 ```typescript -const status = await client.getWorkflowExecution('workflow-id', 'execution-id', { - includeOutput: true -}); -console.log('Status:', status.status); // 'queued', 'running', 'completed', 'failed' +const status = await client.getJobStatus('task-id-from-async-execution'); +console.log('Status:', status.status); // 'queued', 'processing', 'completed', 'failed' if (status.status === 'completed') { - console.log('Output:', status.finalOutput); + console.log('Output:', status.output); } ``` -**Parameters:** -- `workflowId` (string): The workflow ID -- `executionId` (string): The execution ID returned from async execution -- `options.includeOutput` (boolean, optional): Include the final output for completed executions -- `options.selectedOutputs` (string[], optional): Block output selectors to include +**パラメータ:** +- `taskId` (string): 非同期実行から返されたタスクID -**Returns:** `Promise` +**戻り値:** `Promise` -**Response fields:** -- `executionId` (string): The execution ID -- `workflowId` (string): The workflow ID -- `status` (string): One of `'queued'`, `'pending'`, `'running'`, `'paused'`, `'completed'`, `'failed'`, `'cancelled'` -- `startedAt` / `endedAt` (string): Execution timestamps -- `totalDurationMs` (number, nullable): Duration in milliseconds -- `finalOutput` (any, nullable): The workflow output when requested for a completed execution -- `blockOutputs` (object, nullable): Requested block outputs -- `error` (string, nullable): Failure details +**レスポンスフィールド:** +- `success` (boolean): リクエストが成功したかどうか +- `taskId` (string): タスクID +- `status` (string): 次のいずれか `'queued'`, `'processing'`, `'completed'`, `'failed'`, `'cancelled'` +- `metadata` (object): `startedAt`, `completedAt`, および `duration` を含む +- `output` (any, オプション): ワークフロー出力(完了時) +- `error` (any, オプション): エラー詳細(失敗時) +- `estimatedDuration` (number, オプション): 推定所要時間(ミリ秒)(処理中/キュー時) ##### executeWithRetry() @@ -292,10 +286,12 @@ interface WorkflowExecutionResult { ```typescript interface AsyncExecutionResult { success: boolean; - executionId: string; - statusUrl: string; - message: string; - async: true; + taskId: string; + status: 'queued'; + createdAt: string; + links: { + status: string; // e.g., "/api/jobs/{taskId}" + }; } ``` @@ -795,32 +791,29 @@ const client = new SimStudioClient({ async function executeAsync() { try { // Start async execution - const result = await client.executeWorkflow('workflow-id', { data: 'large dataset' }, { + const result = await client.executeWorkflow('workflow-id', { + input: { data: 'large dataset' }, async: true // Execute asynchronously }); // Check if result is an async execution - if ('async' in result && result.async) { - console.log('Execution ID:', result.executionId); - console.log('Status endpoint:', result.statusUrl); + if ('taskId' in result) { + console.log('Task ID:', result.taskId); + console.log('Status endpoint:', result.links.status); // Poll for completion - let status = await client.getWorkflowExecution('workflow-id', result.executionId, { - includeOutput: true - }); + let status = await client.getJobStatus(result.taskId); - while (status.status === 'queued' || status.status === 'pending' || status.status === 'running') { + while (status.status === 'queued' || status.status === 'processing') { console.log('Current status:', status.status); await new Promise(resolve => setTimeout(resolve, 2000)); // Wait 2 seconds - status = await client.getWorkflowExecution('workflow-id', result.executionId, { - includeOutput: true - }); + status = await client.getJobStatus(result.taskId); } if (status.status === 'completed') { console.log('Workflow completed!'); - console.log('Output:', status.finalOutput); - console.log('Duration:', status.totalDurationMs); + console.log('Output:', status.output); + console.log('Duration:', status.metadata.duration); } else { console.error('Workflow failed:', status.error); } diff --git a/apps/docs/content/docs/zh/api-reference/getting-started.mdx b/apps/docs/content/docs/zh/api-reference/getting-started.mdx index e593d678ff9..c8093e72c14 100644 --- a/apps/docs/content/docs/zh/api-reference/getting-started.mdx +++ b/apps/docs/content/docs/zh/api-reference/getting-started.mdx @@ -109,27 +109,27 @@ curl -X POST https://www.sim.ai/api/workflows/{workflowId}/execute \ -d '{"inputs": {}, "async": true}' ``` -This returns immediately with an `executionId` and `statusUrl`: +This returns immediately with a `jobId` and `statusUrl`: ```json { "success": true, - "executionId": "c7a92e15-3f4b-4d8c-a1e6-9b0d5f2c8e74", - "statusUrl": "https://www.sim.ai/api/workflows/{workflowId}/executions/c7a92e15-3f4b-4d8c-a1e6-9b0d5f2c8e74", - "message": "Workflow execution queued", + "jobId": "job_abc123", + "statusUrl": "https://www.sim.ai/api/jobs/job_abc123", + "message": "Workflow execution started", "async": true } ``` -Poll the [Get Execution Status](/api-reference/execution/getWorkflowExecution) endpoint until the status is terminal: +Poll the [Get Job Status](/api-reference/execution/getJobStatus) endpoint until the status is `completed` or `failed`: ```bash -curl https://www.sim.ai/api/workflows/{workflowId}/executions/{executionId}?includeOutput=true \ +curl https://www.sim.ai/api/jobs/{jobId} \ -H "X-API-Key: YOUR_API_KEY" ``` - Execution status transitions follow: `queued` → `running` → `completed`, `failed`, `cancelled`, or `paused`. The `finalOutput` field is populated for completed executions when `includeOutput=true`. + Job status transitions follow: `queued` → `processing` → `completed` or `failed`. The `output` field is only present when status is `completed`. ## Response Format diff --git a/apps/docs/content/docs/zh/api-reference/python.mdx b/apps/docs/content/docs/zh/api-reference/python.mdx index d73bc12083b..608942d1baf 100644 --- a/apps/docs/content/docs/zh/api-reference/python.mdx +++ b/apps/docs/content/docs/zh/api-reference/python.mdx @@ -112,34 +112,30 @@ if is_ready: **返回值:** `bool` -##### get_workflow_execution() +##### get_job_status() -Get the status and optional outputs of a workflow execution. +获取异步任务执行的状态。 ```python -status = client.get_workflow_execution("workflow-id", "execution-id", include_output=True) -print("Status:", status["status"]) # 'queued', 'running', 'completed', 'failed' +status = client.get_job_status("job-id-from-async-execution") +print("Status:", status["status"]) # 'queued', 'processing', 'completed', 'failed' if status["status"] == "completed": - print("Output:", status["finalOutput"]) + print("Output:", status["output"]) ``` -**Parameters:** -- `workflow_id` (str): The workflow ID -- `execution_id` (str): The execution ID returned from async execution -- `include_output` (bool, optional): Include the final output for completed executions -- `selected_outputs` (list[str], optional): Block output selectors to include +**参数:** +- `job_id` (str): 异步执行返回的作业 ID -**Returns:** `Dict[str, Any]` +**返回值:** `Dict[str, Any]` -**Response fields:** -- `executionId` (str): The execution ID -- `workflowId` (str): The workflow ID -- `status` (str): One of `'queued'`, `'pending'`, `'running'`, `'paused'`, `'completed'`, `'failed'`, `'cancelled'` -- `startedAt` / `endedAt` (str): Execution timestamps -- `totalDurationMs` (int, optional): Duration in milliseconds -- `finalOutput` (any, optional): The workflow output when requested for a completed execution -- `blockOutputs` (dict, optional): Requested block outputs -- `error` (str, optional): Failure details +**响应字段:** +- `success` (bool): 请求是否成功 +- `taskId` (str): 作业 ID +- `status` (str): 可能的值包括 `'queued'`, `'processing'`, `'completed'`, `'failed'`, `'cancelled'` +- `metadata` (dict): 包含 `startedAt`, `completedAt` 和 `duration` +- `output` (any, optional): 工作流输出(完成时) +- `error` (any, optional): 错误详情(失败时) +- `estimatedDuration` (int, optional): 估计持续时间(以毫秒为单位,处理中/排队时) ##### execute_with_retry() @@ -275,8 +271,9 @@ class WorkflowExecutionResult: @dataclass class AsyncExecutionResult: success: bool - execution_id: str + job_id: str status_url: str + execution_id: Optional[str] = None message: str = "" async_execution: bool = True ``` @@ -493,31 +490,27 @@ def execute_async(): # Start async execution result = client.execute_workflow( "workflow-id", - input={"data": "large dataset"}, + input_data={"data": "large dataset"}, async_execution=True # Execute asynchronously ) # Check if result is an async execution - if hasattr(result, 'async_execution') and result.async_execution: - print(f"Execution ID: {result.execution_id}") + if hasattr(result, 'job_id'): + print(f"Job ID: {result.job_id}") print(f"Status endpoint: {result.status_url}") # Poll for completion - status = client.get_workflow_execution( - "workflow-id", result.execution_id, include_output=True - ) + status = client.get_job_status(result.job_id) - while status["status"] in ["queued", "pending", "running"]: + while status["status"] in ["queued", "processing"]: print(f"Current status: {status['status']}") time.sleep(2) # Wait 2 seconds - status = client.get_workflow_execution( - "workflow-id", result.execution_id, include_output=True - ) + status = client.get_job_status(result.job_id) if status["status"] == "completed": print("Workflow completed!") - print(f"Output: {status['finalOutput']}") - print(f"Duration: {status['totalDurationMs']}") + print(f"Output: {status['output']}") + print(f"Duration: {status['metadata']['duration']}") else: print(f"Workflow failed: {status['error']}") diff --git a/apps/docs/content/docs/zh/api-reference/typescript.mdx b/apps/docs/content/docs/zh/api-reference/typescript.mdx index 1b3eb86a6af..fac3bdffb73 100644 --- a/apps/docs/content/docs/zh/api-reference/typescript.mdx +++ b/apps/docs/content/docs/zh/api-reference/typescript.mdx @@ -133,37 +133,31 @@ if (isReady) { **返回值:** `Promise` -##### getWorkflowExecution() +##### getJobStatus() -Get the status and optional outputs of a workflow execution. +获取异步任务执行的状态。 ```typescript -const status = await client.getWorkflowExecution('workflow-id', 'execution-id', { - includeOutput: true -}); -console.log('Status:', status.status); // 'queued', 'running', 'completed', 'failed' +const status = await client.getJobStatus('job-id-from-async-execution'); +console.log('Status:', status.status); // 'queued', 'processing', 'completed', 'failed' if (status.status === 'completed') { - console.log('Output:', status.finalOutput); + console.log('Output:', status.output); } ``` -**Parameters:** -- `workflowId` (string): The workflow ID -- `executionId` (string): The execution ID returned from async execution -- `options.includeOutput` (boolean, optional): Include the final output for completed executions -- `options.selectedOutputs` (string[], optional): Block output selectors to include +**参数:** +- `jobId`(字符串):异步执行返回的作业 ID -**Returns:** `Promise` +**返回值:** `Promise` -**Response fields:** -- `executionId` (string): The execution ID -- `workflowId` (string): The workflow ID -- `status` (string): One of `'queued'`, `'pending'`, `'running'`, `'paused'`, `'completed'`, `'failed'`, `'cancelled'` -- `startedAt` / `endedAt` (string): Execution timestamps -- `totalDurationMs` (number, nullable): Duration in milliseconds -- `finalOutput` (any, nullable): The workflow output when requested for a completed execution -- `blockOutputs` (object, nullable): Requested block outputs -- `error` (string, nullable): Failure details +**响应字段:** +- `success`(布尔值):请求是否成功 +- `taskId`(字符串):作业 ID +- `status`(字符串):以下之一 `'queued'`、`'processing'`、`'completed'`、`'failed'`、`'cancelled'` +- `metadata`(对象):包含 `startedAt`、`completedAt` 和 `duration` +- `output`(任意类型,可选):工作流输出(完成时) +- `error`(任意类型,可选):错误详情(失败时) +- `estimatedDuration`(数字,可选):估计持续时间(以毫秒为单位,处理中/排队时) ##### executeWithRetry() @@ -292,8 +286,9 @@ interface WorkflowExecutionResult { ```typescript interface AsyncExecutionResult { success: boolean; - executionId: string; + jobId: string; statusUrl: string; + executionId?: string; message: string; async: true; } @@ -795,32 +790,29 @@ const client = new SimStudioClient({ async function executeAsync() { try { // Start async execution - const result = await client.executeWorkflow('workflow-id', { data: 'large dataset' }, { + const result = await client.executeWorkflow('workflow-id', { + input: { data: 'large dataset' }, async: true // Execute asynchronously }); // Check if result is an async execution - if ('async' in result && result.async) { - console.log('Execution ID:', result.executionId); + if ('jobId' in result) { + console.log('Job ID:', result.jobId); console.log('Status endpoint:', result.statusUrl); // Poll for completion - let status = await client.getWorkflowExecution('workflow-id', result.executionId, { - includeOutput: true - }); + let status = await client.getJobStatus(result.jobId); - while (status.status === 'queued' || status.status === 'pending' || status.status === 'running') { + while (status.status === 'queued' || status.status === 'processing') { console.log('Current status:', status.status); await new Promise(resolve => setTimeout(resolve, 2000)); // Wait 2 seconds - status = await client.getWorkflowExecution('workflow-id', result.executionId, { - includeOutput: true - }); + status = await client.getJobStatus(result.jobId); } if (status.status === 'completed') { console.log('Workflow completed!'); - console.log('Output:', status.finalOutput); - console.log('Duration:', status.totalDurationMs); + console.log('Output:', status.output); + console.log('Duration:', status.metadata.duration); } else { console.error('Workflow failed:', status.error); } diff --git a/apps/docs/content/docs/zh/sdks/python.mdx b/apps/docs/content/docs/zh/sdks/python.mdx index d73bc12083b..c44973c8660 100644 --- a/apps/docs/content/docs/zh/sdks/python.mdx +++ b/apps/docs/content/docs/zh/sdks/python.mdx @@ -112,34 +112,30 @@ if is_ready: **返回值:** `bool` -##### get_workflow_execution() +##### get_job_status() -Get the status and optional outputs of a workflow execution. +获取异步任务执行的状态。 ```python -status = client.get_workflow_execution("workflow-id", "execution-id", include_output=True) -print("Status:", status["status"]) # 'queued', 'running', 'completed', 'failed' +status = client.get_job_status("task-id-from-async-execution") +print("Status:", status["status"]) # 'queued', 'processing', 'completed', 'failed' if status["status"] == "completed": - print("Output:", status["finalOutput"]) + print("Output:", status["output"]) ``` -**Parameters:** -- `workflow_id` (str): The workflow ID -- `execution_id` (str): The execution ID returned from async execution -- `include_output` (bool, optional): Include the final output for completed executions -- `selected_outputs` (list[str], optional): Block output selectors to include +**参数:** +- `task_id` (str): 异步执行返回的任务 ID -**Returns:** `Dict[str, Any]` +**返回值:** `Dict[str, Any]` -**Response fields:** -- `executionId` (str): The execution ID -- `workflowId` (str): The workflow ID -- `status` (str): One of `'queued'`, `'pending'`, `'running'`, `'paused'`, `'completed'`, `'failed'`, `'cancelled'` -- `startedAt` / `endedAt` (str): Execution timestamps -- `totalDurationMs` (int, optional): Duration in milliseconds -- `finalOutput` (any, optional): The workflow output when requested for a completed execution -- `blockOutputs` (dict, optional): Requested block outputs -- `error` (str, optional): Failure details +**响应字段:** +- `success` (bool): 请求是否成功 +- `taskId` (str): 任务 ID +- `status` (str): 可能的值包括 `'queued'`, `'processing'`, `'completed'`, `'failed'`, `'cancelled'` +- `metadata` (dict): 包含 `startedAt`, `completedAt` 和 `duration` +- `output` (any, optional): 工作流输出(完成时) +- `error` (any, optional): 错误详情(失败时) +- `estimatedDuration` (int, optional): 估计持续时间(以毫秒为单位,处理中/排队时) ##### execute_with_retry() @@ -275,10 +271,10 @@ class WorkflowExecutionResult: @dataclass class AsyncExecutionResult: success: bool - execution_id: str - status_url: str - message: str = "" - async_execution: bool = True + task_id: str + status: str # 'queued' + created_at: str + links: Dict[str, str] # e.g., {"status": "/api/jobs/{taskId}"} ``` ### WorkflowStatus @@ -493,31 +489,27 @@ def execute_async(): # Start async execution result = client.execute_workflow( "workflow-id", - input={"data": "large dataset"}, + input_data={"data": "large dataset"}, async_execution=True # Execute asynchronously ) # Check if result is an async execution - if hasattr(result, 'async_execution') and result.async_execution: - print(f"Execution ID: {result.execution_id}") - print(f"Status endpoint: {result.status_url}") + if hasattr(result, 'task_id'): + print(f"Task ID: {result.task_id}") + print(f"Status endpoint: {result.links['status']}") # Poll for completion - status = client.get_workflow_execution( - "workflow-id", result.execution_id, include_output=True - ) + status = client.get_job_status(result.task_id) - while status["status"] in ["queued", "pending", "running"]: + while status["status"] in ["queued", "processing"]: print(f"Current status: {status['status']}") time.sleep(2) # Wait 2 seconds - status = client.get_workflow_execution( - "workflow-id", result.execution_id, include_output=True - ) + status = client.get_job_status(result.task_id) if status["status"] == "completed": print("Workflow completed!") - print(f"Output: {status['finalOutput']}") - print(f"Duration: {status['totalDurationMs']}") + print(f"Output: {status['output']}") + print(f"Duration: {status['metadata']['duration']}") else: print(f"Workflow failed: {status['error']}") diff --git a/apps/docs/content/docs/zh/sdks/typescript.mdx b/apps/docs/content/docs/zh/sdks/typescript.mdx index 1b3eb86a6af..0f038db92dd 100644 --- a/apps/docs/content/docs/zh/sdks/typescript.mdx +++ b/apps/docs/content/docs/zh/sdks/typescript.mdx @@ -133,37 +133,31 @@ if (isReady) { **返回值:** `Promise` -##### getWorkflowExecution() +##### getJobStatus() -Get the status and optional outputs of a workflow execution. +获取异步任务执行的状态。 ```typescript -const status = await client.getWorkflowExecution('workflow-id', 'execution-id', { - includeOutput: true -}); -console.log('Status:', status.status); // 'queued', 'running', 'completed', 'failed' +const status = await client.getJobStatus('task-id-from-async-execution'); +console.log('Status:', status.status); // 'queued', 'processing', 'completed', 'failed' if (status.status === 'completed') { - console.log('Output:', status.finalOutput); + console.log('Output:', status.output); } ``` -**Parameters:** -- `workflowId` (string): The workflow ID -- `executionId` (string): The execution ID returned from async execution -- `options.includeOutput` (boolean, optional): Include the final output for completed executions -- `options.selectedOutputs` (string[], optional): Block output selectors to include +**参数:** +- `taskId`(字符串):异步执行返回的任务 ID -**Returns:** `Promise` +**返回值:** `Promise` -**Response fields:** -- `executionId` (string): The execution ID -- `workflowId` (string): The workflow ID -- `status` (string): One of `'queued'`, `'pending'`, `'running'`, `'paused'`, `'completed'`, `'failed'`, `'cancelled'` -- `startedAt` / `endedAt` (string): Execution timestamps -- `totalDurationMs` (number, nullable): Duration in milliseconds -- `finalOutput` (any, nullable): The workflow output when requested for a completed execution -- `blockOutputs` (object, nullable): Requested block outputs -- `error` (string, nullable): Failure details +**响应字段:** +- `success`(布尔值):请求是否成功 +- `taskId`(字符串):任务 ID +- `status`(字符串):以下之一 `'queued'`、`'processing'`、`'completed'`、`'failed'`、`'cancelled'` +- `metadata`(对象):包含 `startedAt`、`completedAt` 和 `duration` +- `output`(任意类型,可选):工作流输出(完成时) +- `error`(任意类型,可选):错误详情(失败时) +- `estimatedDuration`(数字,可选):估计持续时间(以毫秒为单位,处理中/排队时) ##### executeWithRetry() @@ -292,10 +286,12 @@ interface WorkflowExecutionResult { ```typescript interface AsyncExecutionResult { success: boolean; - executionId: string; - statusUrl: string; - message: string; - async: true; + taskId: string; + status: 'queued'; + createdAt: string; + links: { + status: string; // e.g., "/api/jobs/{taskId}" + }; } ``` @@ -795,32 +791,29 @@ const client = new SimStudioClient({ async function executeAsync() { try { // Start async execution - const result = await client.executeWorkflow('workflow-id', { data: 'large dataset' }, { + const result = await client.executeWorkflow('workflow-id', { + input: { data: 'large dataset' }, async: true // Execute asynchronously }); // Check if result is an async execution - if ('async' in result && result.async) { - console.log('Execution ID:', result.executionId); - console.log('Status endpoint:', result.statusUrl); + if ('taskId' in result) { + console.log('Task ID:', result.taskId); + console.log('Status endpoint:', result.links.status); // Poll for completion - let status = await client.getWorkflowExecution('workflow-id', result.executionId, { - includeOutput: true - }); + let status = await client.getJobStatus(result.taskId); - while (status.status === 'queued' || status.status === 'pending' || status.status === 'running') { + while (status.status === 'queued' || status.status === 'processing') { console.log('Current status:', status.status); await new Promise(resolve => setTimeout(resolve, 2000)); // Wait 2 seconds - status = await client.getWorkflowExecution('workflow-id', result.executionId, { - includeOutput: true - }); + status = await client.getJobStatus(result.taskId); } if (status.status === 'completed') { console.log('Workflow completed!'); - console.log('Output:', status.finalOutput); - console.log('Duration:', status.totalDurationMs); + console.log('Output:', status.output); + console.log('Duration:', status.metadata.duration); } else { console.error('Workflow failed:', status.error); } diff --git a/apps/docs/openapi-core.json b/apps/docs/openapi-core.json index 4df6ecce4ea..97da460bba4 100644 --- a/apps/docs/openapi-core.json +++ b/apps/docs/openapi-core.json @@ -153,7 +153,7 @@ "get": { "operationId": "getWorkflowExecution", "summary": "Get Execution Status", - "description": "Get the current status of a workflow execution. Returns `queued` immediately after async dispatch, then the run's durable lifecycle state (`running`, `paused`, `completed`, `failed`, etc.), timing, error, and optionally per-block outputs. This is the canonical polling resource for synchronous executions, asynchronous executions, and resume attempts.", + "description": "Get the current status of a workflow execution. Returns `queued` immediately after async dispatch, then the run's durable lifecycle state (`running`, `paused`, `completed`, `failed`, etc.), timing, error, and optionally per-block outputs. This legacy-compatible resource remains available for existing integrations.", "tags": ["Execution"], "x-codeSamples": [ { @@ -371,6 +371,66 @@ } } }, + "/api/jobs/{jobId}": { + "get": { + "operationId": "getJobStatus", + "summary": "Get Job Status", + "description": "Poll the status of an asynchronous workflow execution. Use the jobId returned from the Execute Workflow endpoint when the execution is queued asynchronously.", + "tags": ["Execution"], + "x-codeSamples": [ + { + "id": "curl", + "label": "cURL", + "lang": "bash", + "source": "curl -X GET \\\n \"https://www.sim.ai/api/jobs/{jobId}\" \\\n -H \"X-API-Key: YOUR_API_KEY\"" + } + ], + "parameters": [ + { + "name": "jobId", + "in": "path", + "required": true, + "description": "The job identifier returned in the async execution response.", + "schema": { + "type": "string", + "example": "job_4a3b2c1d0e" + } + } + ], + "responses": { + "200": { + "description": "Current status of the job. When completed, includes the execution output.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/JobStatus" + }, + "example": { + "success": true, + "taskId": "job_abc123", + "status": "completed", + "output": { + "content": "Done" + }, + "metadata": { + "startTime": "2026-01-15T10:30:00Z" + } + } + } + } + }, + "401": { + "$ref": "#/components/responses/Unauthorized" + }, + "403": { + "$ref": "#/components/responses/Forbidden" + }, + "404": { + "$ref": "#/components/responses/NotFound" + } + } + } + }, "/api/workflows/{id}/paused": { "get": { "operationId": "listPausedExecutions", @@ -856,7 +916,7 @@ "async": true, "executionId": "f0b3d8c2-7e5a-4b9d-8c1f-6a4e2d0b9c58", "message": "Resume execution queued", - "statusUrl": "https://www.sim.ai/api/workflows/3b1f7c92-8d4e-4a6b-9c0d-5e2f8a714b36/executions/f0b3d8c2-7e5a-4b9d-8c1f-6a4e2d0b9c58" + "statusUrl": "https://www.sim.ai/api/v2/workflows/81f661e1-d704-4861-b5c1-5bb3cf57e6a7/executions/f0b3d8c2-7e5a-4b9d-8c1f-6a4e2d0b9c58" } } } @@ -1333,7 +1393,7 @@ "AsyncExecutionResult": { "type": "object", "description": "Response returned when a workflow execution is queued for asynchronous processing.", - "required": ["success", "async", "executionId", "message", "statusUrl"], + "required": ["success", "async", "jobId", "executionId", "message", "statusUrl"], "properties": { "success": { "type": "boolean", @@ -1345,6 +1405,11 @@ "description": "Always true for async executions. Use this to distinguish from synchronous responses.", "example": true }, + "jobId": { + "type": "string", + "description": "Internal job queue identifier for tracking the execution.", + "example": "job_4a3b2c1d0e" + }, "executionId": { "type": "string", "description": "Unique execution identifier. Use this to query execution status or cancel.", @@ -1359,7 +1424,69 @@ "type": "string", "format": "uri", "description": "URL to poll for execution status and results. Returns the full execution result once complete.", - "example": "https://www.sim.ai/api/workflows/3b1f7c92-8d4e-4a6b-9c0d-5e2f8a714b36/executions/e4f8d2b6-9a1c-4e3d-8b7f-5c0a2d9e6f13" + "example": "https://www.sim.ai/api/jobs/job_4a3b2c1d0e" + } + } + }, + "JobStatus": { + "type": "object", + "description": "Status of an asynchronous job.", + "properties": { + "success": { + "type": "boolean", + "description": "Whether the request was successful.", + "example": true + }, + "taskId": { + "type": "string", + "description": "The unique identifier of the job.", + "example": "job_4a3b2c1d0e" + }, + "status": { + "type": "string", + "enum": ["queued", "processing", "completed", "failed"], + "description": "Current status of the job.", + "example": "completed" + }, + "metadata": { + "type": "object", + "description": "Timing metadata for the job.", + "properties": { + "startedAt": { + "type": "string", + "format": "date-time", + "description": "ISO 8601 timestamp when the job started processing.", + "example": "2025-06-20T14:15:22Z" + }, + "completedAt": { + "type": "string", + "format": "date-time", + "description": "ISO 8601 timestamp when the job completed. Present only when status is completed or failed.", + "example": "2025-06-20T14:15:23Z" + }, + "duration": { + "type": "integer", + "description": "Duration of the job in milliseconds. Present only when status is completed or failed.", + "example": 1250 + } + } + }, + "output": { + "description": "The workflow execution output. Present only when status is completed.", + "type": "object", + "example": { + "result": "Hello, world!" + } + }, + "error": { + "description": "Error details. Present only when status is failed.", + "type": "string", + "example": null + }, + "estimatedDuration": { + "type": "integer", + "description": "Estimated duration in milliseconds. Present only when status is queued or processing.", + "example": 2000 } } }, diff --git a/apps/docs/openapi.json b/apps/docs/openapi.json index 888d3f5e469..21e6bfbb391 100644 --- a/apps/docs/openapi.json +++ b/apps/docs/openapi.json @@ -173,7 +173,7 @@ "get": { "operationId": "getWorkflowExecution", "summary": "Get Execution Status", - "description": "Get the current status of a workflow execution. Returns `queued` immediately after async dispatch, then the run's durable lifecycle state (`running`, `paused`, `completed`, `failed`, etc.), timing, error, and optionally per-block outputs. This is the canonical polling resource for synchronous executions, asynchronous executions, and resume attempts.", + "description": "Get the current status of a workflow execution. Returns `queued` immediately after async dispatch, then the run's durable lifecycle state (`running`, `paused`, `completed`, `failed`, etc.), timing, error, and optionally per-block outputs. This legacy-compatible resource remains available for existing integrations.", "tags": ["Workflows"], "x-codeSamples": [ { @@ -876,7 +876,7 @@ "async": true, "executionId": "f0b3d8c2-7e5a-4b9d-8c1f-6a4e2d0b9c58", "message": "Resume execution queued", - "statusUrl": "https://www.sim.ai/api/workflows/3b1f7c92-8d4e-4a6b-9c0d-5e2f8a714b36/executions/f0b3d8c2-7e5a-4b9d-8c1f-6a4e2d0b9c58" + "statusUrl": "https://www.sim.ai/api/v2/workflows/81f661e1-d704-4861-b5c1-5bb3cf57e6a7/executions/f0b3d8c2-7e5a-4b9d-8c1f-6a4e2d0b9c58" } } } @@ -1725,6 +1725,66 @@ } } }, + "/api/jobs/{jobId}": { + "get": { + "operationId": "getJobStatus", + "summary": "Get Job Status", + "description": "Poll the status of an asynchronous workflow execution. Use the jobId returned from the Execute Workflow endpoint when the execution is queued asynchronously.", + "tags": ["Workflows"], + "x-codeSamples": [ + { + "id": "curl", + "label": "cURL", + "lang": "bash", + "source": "curl -X GET \\\n \"https://www.sim.ai/api/jobs/{jobId}\" \\\n -H \"X-API-Key: YOUR_API_KEY\"" + } + ], + "parameters": [ + { + "name": "jobId", + "in": "path", + "required": true, + "description": "The job identifier returned in the async execution response.", + "schema": { + "type": "string", + "example": "job_4a3b2c1d0e" + } + } + ], + "responses": { + "200": { + "description": "Current status of the job. When completed, includes the execution output.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/JobStatus" + }, + "example": { + "success": true, + "taskId": "job_abc123", + "status": "completed", + "output": { + "content": "Done" + }, + "metadata": { + "startTime": "2026-01-15T10:30:00Z" + } + } + } + } + }, + "401": { + "$ref": "#/components/responses/Unauthorized" + }, + "403": { + "$ref": "#/components/responses/Forbidden" + }, + "404": { + "$ref": "#/components/responses/NotFound" + } + } + } + }, "/api/v1/logs": { "get": { "operationId": "queryLogs", @@ -6550,7 +6610,7 @@ "AsyncExecutionResult": { "type": "object", "description": "Response returned when a workflow execution is queued for asynchronous processing.", - "required": ["success", "async", "executionId", "message", "statusUrl"], + "required": ["success", "async", "jobId", "executionId", "message", "statusUrl"], "properties": { "success": { "type": "boolean", @@ -6562,6 +6622,11 @@ "description": "Always true for async executions. Use this to distinguish from synchronous responses.", "example": true }, + "jobId": { + "type": "string", + "description": "Internal job queue identifier for tracking the execution.", + "example": "job_4a3b2c1d0e" + }, "executionId": { "type": "string", "description": "Unique execution identifier. Use this to query execution status or cancel.", @@ -6576,7 +6641,7 @@ "type": "string", "format": "uri", "description": "URL to poll for execution status and results. Returns the full execution result once complete.", - "example": "https://www.sim.ai/api/workflows/3b1f7c92-8d4e-4a6b-9c0d-5e2f8a714b36/executions/e4f8d2b6-9a1c-4e3d-8b7f-5c0a2d9e6f13" + "example": "https://www.sim.ai/api/jobs/job_4a3b2c1d0e" } } }, @@ -6877,6 +6942,68 @@ } } }, + "JobStatus": { + "type": "object", + "description": "Status of an asynchronous job.", + "properties": { + "success": { + "type": "boolean", + "description": "Whether the request was successful.", + "example": true + }, + "taskId": { + "type": "string", + "description": "The unique identifier of the job.", + "example": "job_4a3b2c1d0e" + }, + "status": { + "type": "string", + "enum": ["queued", "processing", "completed", "failed"], + "description": "Current status of the job.", + "example": "completed" + }, + "metadata": { + "type": "object", + "description": "Timing metadata for the job.", + "properties": { + "startedAt": { + "type": "string", + "format": "date-time", + "description": "ISO 8601 timestamp when the job started processing.", + "example": "2025-06-20T14:15:22Z" + }, + "completedAt": { + "type": "string", + "format": "date-time", + "description": "ISO 8601 timestamp when the job completed. Present only when status is completed or failed.", + "example": "2025-06-20T14:15:23Z" + }, + "duration": { + "type": "integer", + "description": "Duration of the job in milliseconds. Present only when status is completed or failed.", + "example": 1250 + } + } + }, + "output": { + "description": "The workflow execution output. Present only when status is completed.", + "type": "object", + "example": { + "result": "Hello, world!" + } + }, + "error": { + "description": "Error details. Present only when status is failed.", + "type": "string", + "example": null + }, + "estimatedDuration": { + "type": "integer", + "description": "Estimated duration in milliseconds. Present only when status is queued or processing.", + "example": 2000 + } + } + }, "WorkflowExecutionStatus": { "type": "object", "description": "Current status of a workflow execution.", diff --git a/apps/sim/app/api/jobs/[jobId]/route.test.ts b/apps/sim/app/api/jobs/[jobId]/route.test.ts new file mode 100644 index 00000000000..5fc7de85435 --- /dev/null +++ b/apps/sim/app/api/jobs/[jobId]/route.test.ts @@ -0,0 +1,102 @@ +/** + * @vitest-environment node + */ +import { + hybridAuthMockFns, + workflowAuthzMockFns, + workflowsUtilsMock, + workflowsUtilsMockFns, +} from '@sim/testing' +import { NextRequest } from 'next/server' +import { beforeEach, describe, expect, it, vi } from 'vitest' + +const { mockGetJobQueue, mockGetJob } = vi.hoisted(() => ({ + mockGetJobQueue: vi.fn(), + mockGetJob: vi.fn(), +})) + +vi.mock('@/lib/core/async-jobs', () => ({ + getJobQueue: mockGetJobQueue, +})) + +const mockAuthorizeWorkflow = workflowAuthzMockFns.mockAuthorizeWorkflowByWorkspacePermission + +vi.mock('@/lib/workflows/utils', () => workflowsUtilsMock) + +import { GET } from './route' + +function createMockRequest(): NextRequest { + return new NextRequest(new URL('http://localhost:3000/api/jobs/test')) +} + +describe('GET /api/jobs/[jobId]', () => { + beforeEach(() => { + vi.clearAllMocks() + + hybridAuthMockFns.mockCheckHybridAuth.mockResolvedValue({ + success: true, + userId: 'user-1', + apiKeyType: undefined, + workspaceId: undefined, + }) + + mockAuthorizeWorkflow.mockResolvedValue({ allowed: true, status: 200 }) + workflowsUtilsMockFns.mockGetWorkflowById.mockResolvedValue({ + id: 'workflow-1', + workspaceId: 'workspace-1', + }) + + mockGetJobQueue.mockResolvedValue({ + getJob: mockGetJob, + }) + }) + + it('returns job status with metadata', async () => { + mockGetJob.mockResolvedValue({ + id: 'job-1', + status: 'pending', + metadata: { + workflowId: 'workflow-1', + }, + }) + + const response = await GET(createMockRequest(), { + params: Promise.resolve({ jobId: 'job-1' }), + }) + const body = await response.json() + + expect(response.status).toBe(200) + expect(body.status).toBe('pending') + expect(body.metadata.workflowId).toBe('workflow-1') + }) + + it('returns completed output from job', async () => { + mockGetJob.mockResolvedValue({ + id: 'job-2', + status: 'completed', + metadata: { + workflowId: 'workflow-1', + }, + output: { success: true }, + }) + + const response = await GET(createMockRequest(), { + params: Promise.resolve({ jobId: 'job-2' }), + }) + const body = await response.json() + + expect(response.status).toBe(200) + expect(body.status).toBe('completed') + expect(body.output).toEqual({ success: true }) + }) + + it('returns 404 when job does not exist', async () => { + mockGetJob.mockResolvedValue(null) + + const response = await GET(createMockRequest(), { + params: Promise.resolve({ jobId: 'missing-job' }), + }) + + expect(response.status).toBe(404) + }) +}) diff --git a/apps/sim/app/api/jobs/[jobId]/route.ts b/apps/sim/app/api/jobs/[jobId]/route.ts new file mode 100644 index 00000000000..01677e506ee --- /dev/null +++ b/apps/sim/app/api/jobs/[jobId]/route.ts @@ -0,0 +1,90 @@ +import { createLogger } from '@sim/logger' +import { toError } from '@sim/utils/errors' +import { type NextRequest, NextResponse } from 'next/server' +import { getJobStatusContract } from '@/lib/api/contracts/common' +import { parseRequest } from '@/lib/api/server' +import { checkHybridAuth } from '@/lib/auth/hybrid' +import { getJobQueue } from '@/lib/core/async-jobs' +import { generateRequestId } from '@/lib/core/utils/request' +import { withRouteHandler } from '@/lib/core/utils/with-route-handler' +import { createErrorResponse } from '@/app/api/workflows/utils' + +const logger = createLogger('TaskStatusAPI') + +export const GET = withRouteHandler( + async (request: NextRequest, context: { params: Promise<{ jobId: string }> }) => { + const parsed = await parseRequest(getJobStatusContract, request, context) + if (!parsed.success) return parsed.response + const { jobId: taskId } = parsed.data.params + const requestId = generateRequestId() + + try { + const authResult = await checkHybridAuth(request, { requireWorkflowId: false }) + if (!authResult.success || !authResult.userId) { + logger.warn(`[${requestId}] Unauthorized task status request`) + return createErrorResponse(authResult.error || 'Authentication required', 401) + } + + const authenticatedUserId = authResult.userId + + const jobQueue = await getJobQueue() + const job = await jobQueue.getJob(taskId) + + if (!job) { + return createErrorResponse('Task not found', 404) + } + + const metadataToCheck = job.metadata + + if (metadataToCheck?.workflowId) { + const { authorizeWorkflowByWorkspacePermission } = await import( + '@sim/platform-authz/workflow' + ) + const accessCheck = await authorizeWorkflowByWorkspacePermission({ + userId: authenticatedUserId, + workflowId: metadataToCheck.workflowId as string, + action: 'read', + }) + if (!accessCheck.allowed) { + logger.warn(`[${requestId}] Access denied to workflow ${metadataToCheck.workflowId}`) + return createErrorResponse('Access denied', 403) + } + + if (authResult.apiKeyType === 'workspace' && authResult.workspaceId) { + const { getWorkflowById } = await import('@/lib/workflows/utils') + const workflow = await getWorkflowById(metadataToCheck.workflowId as string) + if (!workflow?.workspaceId || workflow.workspaceId !== authResult.workspaceId) { + return createErrorResponse('API key is not authorized for this workspace', 403) + } + } + } else if (metadataToCheck?.userId && metadataToCheck.userId !== authenticatedUserId) { + logger.warn(`[${requestId}] Access denied to user ${metadataToCheck.userId}`) + return createErrorResponse('Access denied', 403) + } else if (!metadataToCheck?.userId && !metadataToCheck?.workflowId) { + logger.warn(`[${requestId}] Access denied to job ${taskId}`) + return createErrorResponse('Access denied', 403) + } + + const response: Record = { + success: true, + taskId, + status: job.status, + metadata: job.metadata, + } + + if (job.output !== undefined) response.output = job.output + if (job.error !== undefined) response.error = job.error + + return NextResponse.json(response) + } catch (error: unknown) { + const errorMessage = toError(error).message + logger.error(`[${requestId}] Error fetching task status:`, error) + + if (errorMessage?.includes('not found')) { + return createErrorResponse('Task not found', 404) + } + + return createErrorResponse('Failed to fetch task status', 500) + } + } +) diff --git a/apps/sim/app/api/resume/[workflowId]/[executionId]/[contextId]/route.test.ts b/apps/sim/app/api/resume/[workflowId]/[executionId]/[contextId]/route.test.ts index a1bf0f30c2b..f63098a3e26 100644 --- a/apps/sim/app/api/resume/[workflowId]/[executionId]/[contextId]/route.test.ts +++ b/apps/sim/app/api/resume/[workflowId]/[executionId]/[contextId]/route.test.ts @@ -263,7 +263,7 @@ describe('POST /api/resume/[workflowId]/[executionId]/[contextId]', () => { async: true, executionId: 'resume-execution-1', message: 'Resume execution queued', - statusUrl: 'https://test.sim.ai/api/workflows/workflow-1/executions/resume-execution-1', + statusUrl: 'https://test.sim.ai/api/v2/workflows/workflow-1/executions/resume-execution-1', }) expect(mockEnqueueResume).toHaveBeenCalledWith( 'resume-execution', diff --git a/apps/sim/app/api/resume/[workflowId]/[executionId]/[contextId]/route.ts b/apps/sim/app/api/resume/[workflowId]/[executionId]/[contextId]/route.ts index 6a3ffa5ab5c..0864ac89a6c 100644 --- a/apps/sim/app/api/resume/[workflowId]/[executionId]/[contextId]/route.ts +++ b/apps/sim/app/api/resume/[workflowId]/[executionId]/[contextId]/route.ts @@ -359,7 +359,7 @@ export const POST = withRouteHandler( async: true, executionId: enqueueResult.resumeExecutionId, message: 'Resume execution queued', - statusUrl: `${getBaseUrl()}/api/workflows/${workflowId}/executions/${enqueueResult.resumeExecutionId}`, + statusUrl: `${getBaseUrl()}/api/v2/workflows/${workflowId}/executions/${enqueueResult.resumeExecutionId}`, }, { status: 202 } ) diff --git a/apps/sim/app/api/workflows/[id]/execute/route.async.test.ts b/apps/sim/app/api/workflows/[id]/execute/route.async.test.ts index a073abdd8bf..f63a9e90b76 100644 --- a/apps/sim/app/api/workflows/[id]/execute/route.async.test.ts +++ b/apps/sim/app/api/workflows/[id]/execute/route.async.test.ts @@ -806,10 +806,8 @@ describe('workflow execute async route', () => { expect(response.status).toBe(202) expect(body.executionId).toBe('execution-123') - expect(body).not.toHaveProperty('jobId') - expect(body.statusUrl).toBe( - 'http://localhost:3000/api/workflows/workflow-1/executions/execution-123' - ) + expect(body.jobId).toBe('job-123') + expect(body.statusUrl).toBe('http://localhost:3000/api/jobs/job-123') expect(mockClaimExecutionId).toHaveBeenCalledWith('execution-123') expect(mockEnqueue).toHaveBeenCalledWith( 'workflow-execution', diff --git a/apps/sim/app/api/workflows/[id]/execute/route.ts b/apps/sim/app/api/workflows/[id]/execute/route.ts index 7a638f1c47b..9817acc81b0 100644 --- a/apps/sim/app/api/workflows/[id]/execute/route.ts +++ b/apps/sim/app/api/workflows/[id]/execute/route.ts @@ -429,9 +429,10 @@ async function handleAsyncExecution(params: AsyncExecutionParams): Promise - } catch { - return { input: 'your data here' } - } + return JSON.parse(match[1]) as Record } - return { input: 'your data here' } + return { input: {} } } const getStreamPayloadObject = (): Record => { @@ -261,21 +257,21 @@ while (true) { const getAsyncCommand = (): string => { if (!info) return '' const endpoint = getBaseEndpoint() - if (!endpoint.endsWith('/execute')) { + const v2WorkflowPrefix = '/api/v2/workflows/' + if (!endpoint.includes(v2WorkflowPrefix) || !endpoint.endsWith('/execute')) { throw new Error(`Invalid workflow execution endpoint: ${endpoint}`) } - const baseUrl = endpoint.split('/api/workflows/')[0] + const baseUrl = endpoint.split(v2WorkflowPrefix)[0] const statusEndpoint = `${endpoint.slice(0, -'/execute'.length)}/executions/EXECUTION_ID_FROM_EXECUTION` - const payload = getPayloadObject() - const isPublic = info.isPublicApi + const payload = { ...getPayloadObject(), async: true } switch (asyncExampleType) { case 'execute': switch (language) { case 'curl': return `curl -X POST \\ -${isPublic ? '' : ' -H "X-API-Key: $SIM_API_KEY" \\\n'} -H "Content-Type: application/json" \\ - -H "X-Execution-Mode: async" \\ + -H "X-API-Key: $SIM_API_KEY" \\ + -H "Content-Type: application/json" \\ -d '${JSON.stringify(payload)}' \\ ${endpoint}` @@ -286,39 +282,39 @@ import requests response = requests.post( "${endpoint}", headers={ -${isPublic ? '' : ' "X-API-Key": os.environ.get("SIM_API_KEY"),\n'} "Content-Type": "application/json", - "X-Execution-Mode": "async" + "X-API-Key": os.environ.get("SIM_API_KEY"), + "Content-Type": "application/json", }, json=${JSON.stringify(payload, null, 4).replace(/\n/g, '\n ')} ) -execution = response.json() +execution = response.json()["data"] print(execution)` case 'javascript': return `const response = await fetch("${endpoint}", { method: "POST", headers: { -${isPublic ? '' : ' "X-API-Key": process.env.SIM_API_KEY,\n'} "Content-Type": "application/json", - "X-Execution-Mode": "async" + "X-API-Key": process.env.SIM_API_KEY, + "Content-Type": "application/json", }, body: JSON.stringify(${JSON.stringify(payload)}) }); -const execution = await response.json(); +const { data: execution } = await response.json(); console.log(execution);` case 'typescript': return `const response = await fetch("${endpoint}", { method: "POST", headers: { -${isPublic ? '' : ' "X-API-Key": process.env.SIM_API_KEY,\n'} "Content-Type": "application/json", - "X-Execution-Mode": "async" + "X-API-Key": process.env.SIM_API_KEY, + "Content-Type": "application/json", }, body: JSON.stringify(${JSON.stringify(payload)}) }); -const execution: { executionId: string; statusUrl: string } = await response.json(); +const { data: execution }: { data: { executionId: string; statusUrl: string } } = await response.json(); console.log(execution);` default: @@ -341,7 +337,7 @@ response = requests.get( headers={"X-API-Key": os.environ.get("SIM_API_KEY")} ) -status = response.json() +status = response.json()["data"] print(status)` case 'javascript': @@ -352,7 +348,7 @@ print(status)` } ); -const status = await response.json(); +const { data: status } = await response.json(); console.log(status);` case 'typescript': @@ -363,7 +359,7 @@ console.log(status);` } ); -const status: Record = await response.json(); +const { data: status }: { data: Record } = await response.json(); console.log(status);` default: diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/components/deploy-modal/deploy-modal.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/components/deploy-modal/deploy-modal.tsx index d46e57a420c..9e5e643733d 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/components/deploy-modal/deploy-modal.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/components/deploy-modal/deploy-modal.tsx @@ -226,7 +226,21 @@ export function DeployModal({ workflowWorkspaceId ? 'YOUR_WORKSPACE_API_KEY' : 'YOUR_PERSONAL_API_KEY' const getInputFormatExample = (includeStreaming = false) => { - return getInputFormatExampleUtil(includeStreaming, selectedStreamingOutputs) + const inputFormatExample = getInputFormatExampleUtil(includeStreaming, selectedStreamingOutputs) + if (!inputFormatExample) return '' + + const match = inputFormatExample.match(/-d\s*'([\s\S]*)'/) + if (!match) { + throw new Error(`Invalid workflow input example: ${inputFormatExample}`) + } + + const legacyBody = JSON.parse(match[1]) as Record + const { stream, selectedOutputs, ...input } = legacyBody + return ` -d '${JSON.stringify({ + input, + ...(stream === true ? { stream: true } : {}), + ...(Array.isArray(selectedOutputs) ? { selectedOutputs } : {}), + })}'` } const deploymentInfo: WorkflowDeploymentInfoUI | null = (() => { @@ -234,7 +248,7 @@ export function DeployModal({ return null } - const endpoint = `${getBaseUrl()}/api/workflows/${workflowId}/execute` + const endpoint = `${getBaseUrl()}/api/v2/workflows/${workflowId}/execute` const inputFormatExample = getInputFormatExample(selectedStreamingOutputs.length > 0) const placeholderKey = getApiHeaderPlaceholder() diff --git a/apps/sim/lib/api/contracts/common.ts b/apps/sim/lib/api/contracts/common.ts index 6d2227d51d0..d79833499db 100644 --- a/apps/sim/lib/api/contracts/common.ts +++ b/apps/sim/lib/api/contracts/common.ts @@ -1,4 +1,5 @@ import { z } from 'zod' +import { jobIdParamsSchema } from '@/lib/api/contracts/primitives' import { defineRouteContract } from '@/lib/api/contracts/types' const NO_EMAIL_HEADER_CONTROL_CHARS_REGEX = /^[^\r\n\u0000-\u001F\u007F]+$/ @@ -104,3 +105,26 @@ export const getStatusContract = defineRouteContract({ }), }, }) + +const jobStatusSchema = z.enum(['pending', 'processing', 'completed', 'failed']) + +const jobStatusResponseSchema = z + .object({ + success: z.literal(true), + taskId: z.string(), + status: jobStatusSchema, + metadata: z.record(z.string(), z.unknown()).nullable().optional(), + output: z.unknown().optional(), + error: z.string().optional(), + }) + .passthrough() + +export const getJobStatusContract = defineRouteContract({ + method: 'GET', + path: '/api/jobs/[jobId]', + params: jobIdParamsSchema, + response: { + mode: 'json', + schema: jobStatusResponseSchema, + }, +}) diff --git a/apps/sim/lib/api/contracts/primitives.ts b/apps/sim/lib/api/contracts/primitives.ts index b66db42f85a..142e0990a54 100644 --- a/apps/sim/lib/api/contracts/primitives.ts +++ b/apps/sim/lib/api/contracts/primitives.ts @@ -81,6 +81,10 @@ export function isCanonicalBase64(value: string): boolean { return true } +export const jobIdParamsSchema = z.object({ + jobId: z.string().min(1), +}) + /** * Non-empty string identifier with no custom message — suitable for internal * shapes where the field name is not worth surfacing. For a required *request* diff --git a/apps/sim/lib/api/contracts/workflows.ts b/apps/sim/lib/api/contracts/workflows.ts index e71a29a46bd..a4e5743211d 100644 --- a/apps/sim/lib/api/contracts/workflows.ts +++ b/apps/sim/lib/api/contracts/workflows.ts @@ -634,6 +634,7 @@ const resumeWorkflowExecutionContextResponseSchema = z async: z.boolean().optional(), executionId: z.string().optional(), queuePosition: z.number().optional(), + jobId: z.string().optional(), output: z.unknown().optional(), error: z.string().optional(), metadata: z diff --git a/apps/sim/lib/copilot/tools/handlers/deployment/deploy.ts b/apps/sim/lib/copilot/tools/handlers/deployment/deploy.ts index d93c303c232..cb97bedf8e8 100644 --- a/apps/sim/lib/copilot/tools/handlers/deployment/deploy.ts +++ b/apps/sim/lib/copilot/tools/handlers/deployment/deploy.ts @@ -31,7 +31,7 @@ import type { DeployApiParams, DeployChatParams, DeployMcpParams } from '../para import { getCopilotDeploymentIdempotencyKey, getHistoricalDeploymentAttemptError } from './context' function buildWorkflowApiEndpoint(baseUrl: string, workflowId: string): string { - return `${baseUrl}/api/workflows/${workflowId}/execute` + return `${baseUrl}/api/v2/workflows/${workflowId}/execute` } function buildWorkflowExecutionStatusEndpoint( @@ -39,7 +39,10 @@ function buildWorkflowExecutionStatusEndpoint( apiEndpoint: string, executionId: string ): string { - if (!apiEndpoint.startsWith(`${baseUrl}/api/workflows/`) || !apiEndpoint.endsWith('/execute')) { + if ( + !apiEndpoint.startsWith(`${baseUrl}/api/v2/workflows/`) || + !apiEndpoint.endsWith('/execute') + ) { throw new Error(`Invalid workflow execution endpoint: ${apiEndpoint}`) } return `${apiEndpoint.slice(0, -'/execute'.length)}/executions/${executionId}` @@ -69,8 +72,7 @@ function buildWorkflowApiConfig(baseUrl: string, apiEndpoint: string) { method: 'POST', transport: 'json', stream: false, - headers: { 'X-Execution-Mode': 'async' }, - body: { input: { key: 'value' } }, + body: { async: true, input: { key: 'value' } }, executionStatusEndpointTemplate: buildWorkflowExecutionStatusEndpoint( baseUrl, apiEndpoint, @@ -94,8 +96,7 @@ function buildWorkflowApiExamples(baseUrl: string, apiEndpoint: string) { async: `curl -X POST "${apiEndpoint}" \\ -H "Content-Type: application/json" \\ -H "X-API-Key: YOUR_API_KEY" \\ - -H "X-Execution-Mode: async" \\ - -d '{"input":{"key":"value"}}'`, + -d '{"async":true,"input":{"key":"value"}}'`, poll: `curl "${buildWorkflowExecutionStatusEndpoint(baseUrl, apiEndpoint, 'EXECUTION_ID')}" \\ -H "X-API-Key: YOUR_API_KEY"`, } diff --git a/packages/python-sdk/README.md b/packages/python-sdk/README.md index f4ecbc680ac..390649be1cf 100644 --- a/packages/python-sdk/README.md +++ b/packages/python-sdk/README.md @@ -48,10 +48,10 @@ SimStudioClient(api_key: str, base_url: str = "https://sim.ai") Execute a workflow with optional input data. ```python -# With dict input (spread at root level of request body) +# With dict input (sent as the v2 input object) result = client.execute_workflow("workflow-id", {"message": "Hello, world!"}) -# With primitive input (wrapped as { input: value }) +# With primitive input (sent as { input: { input: value } }) result = client.execute_workflow("workflow-id", "NVDA") # With options (keyword-only arguments) @@ -60,7 +60,7 @@ result = client.execute_workflow("workflow-id", {"message": "Hello"}, timeout=60 **Parameters:** - `workflow_id` (str): The ID of the workflow to execute -- `input` (any, optional): Input data to pass to the workflow. Dicts are spread at the root level, primitives/lists are wrapped in `{ input: value }`. File objects are automatically converted to base64. +- `input` (any, optional): Input data to pass to the workflow. Dicts become the v2 `input` object; primitives and lists become `{ input: value }` inside it. File objects are automatically converted to base64. - `timeout` (float, keyword-only): Timeout in seconds (default: 30.0) - `stream` (bool, keyword-only): Enable streaming responses - `selected_outputs` (list, keyword-only): Block outputs to stream (e.g., `["agent1.content"]`) @@ -137,6 +137,16 @@ print("Execution status:", status["status"]) **Returns:** `dict` +##### get_job_status(job_id) + +Get the status of a job created through the legacy async execution endpoint. New integrations should use `get_workflow_execution()` with an execution ID. + +```python +status = client.get_job_status("legacy-job-id") +``` + +**Returns:** `dict` + ##### execute_with_retry(workflow_id, input=None, *, timeout=30.0, stream=None, selected_outputs=None, async_execution=None, max_retries=3, initial_delay=1.0, max_delay=30.0, backoff_multiplier=2.0) Execute a workflow with automatic retry on rate limit errors. diff --git a/packages/python-sdk/simstudio/__init__.py b/packages/python-sdk/simstudio/__init__.py index 1fcd255ee2c..e930e2467ba 100644 --- a/packages/python-sdk/simstudio/__init__.py +++ b/packages/python-sdk/simstudio/__init__.py @@ -178,31 +178,26 @@ def execute_workflow( Raises: SimStudioError: If the workflow execution fails """ - url = f"{self.base_url}/api/workflows/{workflow_id}/execute" - - # Build headers - async execution uses X-Execution-Mode header + url = f"{self.base_url}/api/v2/workflows/{workflow_id}/execute" headers = self._session.headers.copy() - if async_execution: - headers['X-Execution-Mode'] = 'async' try: - # Build JSON body - spread dict inputs at root level, wrap primitives/lists in 'input' field - body = {} + workflow_input = {} if input is not None: if isinstance(input, dict): - # Dict input: spread at root level (matches curl/API behavior) - body = input.copy() + workflow_input = input.copy() else: - # Primitive or list input: wrap in 'input' field - body = {'input': input} + workflow_input = {'input': input} - # Convert any file objects in the input to base64 format - body = self._convert_files_to_base64(body) + workflow_input = self._convert_files_to_base64(workflow_input) + body = {'input': workflow_input} if stream is not None: body['stream'] = stream if selected_outputs is not None: body['selectedOutputs'] = selected_outputs + if async_execution is not None: + body['async'] = async_execution response = self._session.post( url, @@ -226,34 +221,41 @@ def execute_workflow( if not response.ok: try: error_data = response.json() - error_message = error_data.get('error', f'HTTP {response.status_code}: {response.reason}') - error_code = error_data.get('code') + error = error_data.get('error', {}) + error_message = error.get('message', f'HTTP {response.status_code}: {response.reason}') + error_code = error.get('code') except (ValueError, KeyError): error_message = f'HTTP {response.status_code}: {response.reason}' error_code = None raise SimStudioError(error_message, error_code, response.status_code) - result_data = response.json() + result = response.json() + if 'data' not in result: + raise SimStudioError('Invalid v2 workflow execution response', 'EXECUTION_ERROR') + result_data = result['data'] - # Check if this is an async execution response (202 status) - if response.status_code == 202 and 'executionId' in result_data: + if response.status_code == 202: + if 'executionId' not in result_data or 'statusUrl' not in result_data: + raise SimStudioError('Invalid v2 async execution response', 'EXECUTION_ERROR') return AsyncExecutionResult( - success=result_data.get('success', True), + success=True, execution_id=result_data['executionId'], status_url=result_data['statusUrl'], - message=result_data.get('message', ''), - async_execution=result_data.get('async', True) + message='Workflow execution queued', + async_execution=True ) + execution_error = result_data.get('error') return WorkflowExecutionResult( - success=result_data['success'], + success=result_data.get('status') != 'failed', output=result_data.get('output'), - error=result_data.get('error'), - logs=result_data.get('logs'), - metadata=result_data.get('metadata'), - trace_spans=result_data.get('traceSpans'), - total_duration=result_data.get('totalDuration') + error=execution_error.get('message') if execution_error else None, + metadata={ + 'duration': result_data.get('durationMs'), + 'executionId': result_data['executionId'] + }, + total_duration=result_data.get('durationMs') ) except requests.Timeout: @@ -374,6 +376,42 @@ def close(self) -> None: """Close the underlying HTTP session.""" self._session.close() + def get_job_status(self, job_id: str) -> Dict[str, Any]: + """ + Get the status of a legacy async job. + + Args: + job_id: The job ID returned from legacy async execution + + Returns: + Dictionary containing the job status + + Raises: + SimStudioError: If getting the status fails + """ + url = f"{self.base_url}/api/jobs/{job_id}" + + try: + response = self._session.get(url) + + self._update_rate_limit_info(response) + + if not response.ok: + try: + error_data = response.json() + error_message = error_data.get('error', f'HTTP {response.status_code}: {response.reason}') + error_code = error_data.get('code') + except (ValueError, KeyError): + error_message = f'HTTP {response.status_code}: {response.reason}' + error_code = None + + raise SimStudioError(error_message, error_code, response.status_code) + + return response.json() + + except requests.RequestException as e: + raise SimStudioError(f'Failed to get job status: {str(e)}', 'STATUS_ERROR') + def get_workflow_execution( self, workflow_id: str, @@ -383,7 +421,7 @@ def get_workflow_execution( selected_outputs: Optional[list] = None ) -> Dict[str, Any]: """ - Get a workflow execution's current status and optional outputs. + Get a workflow execution's current status and optional outputs from the v2 API. Args: workflow_id: The workflow ID @@ -397,7 +435,7 @@ def get_workflow_execution( Raises: SimStudioError: If getting the status fails """ - url = f"{self.base_url}/api/workflows/{workflow_id}/executions/{execution_id}" + url = f"{self.base_url}/api/v2/workflows/{workflow_id}/executions/{execution_id}" params = {} if include_output is not None: params['includeOutput'] = str(include_output).lower() @@ -412,15 +450,19 @@ def get_workflow_execution( if not response.ok: try: error_data = response.json() - error_message = error_data.get('error', f'HTTP {response.status_code}: {response.reason}') - error_code = error_data.get('code') + error = error_data.get('error', {}) + error_message = error.get('message', f'HTTP {response.status_code}: {response.reason}') + error_code = error.get('code') except (ValueError, KeyError): error_message = f'HTTP {response.status_code}: {response.reason}' error_code = None raise SimStudioError(error_message, error_code, response.status_code) - return response.json() + result = response.json() + if 'data' not in result: + raise SimStudioError('Invalid v2 workflow execution response', 'STATUS_ERROR') + return result['data'] except requests.RequestException as e: raise SimStudioError(f'Failed to get workflow execution: {str(e)}', 'STATUS_ERROR') diff --git a/packages/python-sdk/tests/test_client.py b/packages/python-sdk/tests/test_client.py index 52d5802b74f..8473758198b 100644 --- a/packages/python-sdk/tests/test_client.py +++ b/packages/python-sdk/tests/test_client.py @@ -7,6 +7,19 @@ from simstudio import SimStudioClient, SimStudioError, WorkflowExecutionResult, WorkflowStatus +def v2_execution_response(output=None): + return { + "data": { + "executionId": "execution-123", + "workflowId": "workflow-id", + "status": "completed", + "output": {} if output is None else output, + "error": None, + "durationMs": 10 + } + } + + def test_simstudio_client_initialization(): """Test SimStudioClient initialization.""" client = SimStudioClient(api_key="test-api-key", base_url="https://test.sim.ai") @@ -101,11 +114,10 @@ def test_async_execution_returns_execution_id(mock_post): mock_response.ok = True mock_response.status_code = 202 mock_response.json.return_value = { - "success": True, - "executionId": "execution-123", - "statusUrl": "https://test.sim.ai/api/workflows/workflow-id/executions/execution-123", - "message": "Workflow execution started", - "async": True + "data": { + "executionId": "execution-123", + "statusUrl": "https://sim.ai/api/v2/workflows/workflow-id/executions/execution-123" + } } mock_response.headers.get.return_value = None mock_post.return_value = mock_response @@ -119,11 +131,16 @@ def test_async_execution_returns_execution_id(mock_post): assert result.success is True assert result.execution_id == "execution-123" - assert result.status_url == "https://test.sim.ai/api/workflows/workflow-id/executions/execution-123" + assert result.status_url == "https://sim.ai/api/v2/workflows/workflow-id/executions/execution-123" assert result.async_execution is True call_args = mock_post.call_args - assert call_args[1]["headers"]["X-Execution-Mode"] == "async" + assert call_args.args[0] == "https://sim.ai/api/v2/workflows/workflow-id/execute" + assert "X-Execution-Mode" not in call_args.kwargs["headers"] + assert call_args.kwargs["json"] == { + "input": {"message": "Hello"}, + "async": True + } @patch('simstudio.requests.Session.post') @@ -132,11 +149,7 @@ def test_sync_execution_returns_result(mock_post): mock_response = Mock() mock_response.ok = True mock_response.status_code = 200 - mock_response.json.return_value = { - "success": True, - "output": {"result": "completed"}, - "logs": [] - } + mock_response.json.return_value = v2_execution_response({"result": "completed"}) mock_response.headers.get.return_value = None mock_post.return_value = mock_response @@ -158,7 +171,7 @@ def test_async_header_not_set_when_false(mock_post): mock_response = Mock() mock_response.ok = True mock_response.status_code = 200 - mock_response.json.return_value = {"success": True, "output": {}} + mock_response.json.return_value = v2_execution_response() mock_response.headers.get.return_value = None mock_post.return_value = mock_response @@ -170,15 +183,61 @@ def test_async_header_not_set_when_false(mock_post): @patch('simstudio.requests.Session.get') -def test_get_workflow_execution_success(mock_get): - """Test getting workflow execution status.""" +def test_get_job_status_success(mock_get): + """Test getting legacy job status.""" mock_response = Mock() mock_response.ok = True mock_response.json.return_value = { - "executionId": "execution-123", - "workflowId": "workflow-123", + "success": True, + "taskId": "task-123", "status": "completed", - "finalOutput": {"result": "done"} + "metadata": {"duration": 60000}, + "output": {"result": "done"} + } + mock_response.headers.get.return_value = None + mock_get.return_value = mock_response + + client = SimStudioClient(api_key="test-api-key", base_url="https://test.sim.ai") + result = client.get_job_status("task-123") + + assert result["taskId"] == "task-123" + assert result["status"] == "completed" + assert result["output"]["result"] == "done" + mock_get.assert_called_once_with("https://test.sim.ai/api/jobs/task-123") + + +@patch('simstudio.requests.Session.get') +def test_get_job_status_not_found(mock_get): + """Test legacy job not found error.""" + mock_response = Mock() + mock_response.ok = False + mock_response.status_code = 404 + mock_response.reason = "Not Found" + mock_response.json.return_value = { + "error": "Job not found", + "code": "JOB_NOT_FOUND" + } + mock_response.headers.get.return_value = None + mock_get.return_value = mock_response + + client = SimStudioClient(api_key="test-api-key") + + with pytest.raises(SimStudioError) as exc_info: + client.get_job_status("invalid-task") + assert "Job not found" in str(exc_info.value) + + +@patch('simstudio.requests.Session.get') +def test_get_workflow_execution_success(mock_get): + mock_response = Mock() + mock_response.ok = True + mock_response.json.return_value = { + "data": { + "executionId": "execution-123", + "workflowId": "workflow-123", + "status": "completed", + "output": {"result": "done"} + } } mock_response.headers.get.return_value = None mock_get.return_value = mock_response @@ -193,23 +252,24 @@ def test_get_workflow_execution_success(mock_get): assert result["executionId"] == "execution-123" assert result["status"] == "completed" - assert result["finalOutput"]["result"] == "done" + assert result["output"]["result"] == "done" mock_get.assert_called_once_with( - "https://test.sim.ai/api/workflows/workflow-123/executions/execution-123", + "https://test.sim.ai/api/v2/workflows/workflow-123/executions/execution-123", params={"includeOutput": "true", "selectedOutputs": "agent.content"} ) @patch('simstudio.requests.Session.get') def test_get_workflow_execution_not_found(mock_get): - """Test execution not found error.""" mock_response = Mock() mock_response.ok = False mock_response.status_code = 404 mock_response.reason = "Not Found" mock_response.json.return_value = { - "error": "Execution not found", - "code": "EXECUTION_NOT_FOUND" + "error": { + "code": "NOT_FOUND", + "message": "Execution not found" + } } mock_response.headers.get.return_value = None mock_get.return_value = mock_response @@ -228,10 +288,7 @@ def test_execute_with_retry_success_first_attempt(mock_sleep, mock_post): mock_response = Mock() mock_response.ok = True mock_response.status_code = 200 - mock_response.json.return_value = { - "success": True, - "output": {"result": "success"} - } + mock_response.json.return_value = v2_execution_response({"result": "success"}) mock_response.headers.get.return_value = None mock_post.return_value = mock_response @@ -265,10 +322,7 @@ def test_execute_with_retry_retries_on_rate_limit(mock_sleep, mock_post): success_response = Mock() success_response.ok = True success_response.status_code = 200 - success_response.json.return_value = { - "success": True, - "output": {"result": "success"} - } + success_response.json.return_value = v2_execution_response({"result": "success"}) success_response.headers.get.return_value = None mock_post.side_effect = [rate_limit_response, success_response] @@ -322,8 +376,10 @@ def test_execute_with_retry_no_retry_on_other_errors(mock_post): mock_response.status_code = 500 mock_response.reason = "Internal Server Error" mock_response.json.return_value = { - "error": "Server error", - "code": "INTERNAL_ERROR" + "error": { + "code": "INTERNAL_ERROR", + "message": "Server error" + } } mock_response.headers.get.return_value = None mock_post.return_value = mock_response @@ -350,7 +406,7 @@ def test_get_rate_limit_info_after_api_call(mock_post): mock_response = Mock() mock_response.ok = True mock_response.status_code = 200 - mock_response.json.return_value = {"success": True, "output": {}} + mock_response.json.return_value = v2_execution_response() mock_response.headers.get.side_effect = lambda h: { 'x-ratelimit-limit': '100', 'x-ratelimit-remaining': '95', @@ -437,7 +493,7 @@ def test_execute_workflow_with_stream_and_selected_outputs(mock_post): mock_response = Mock() mock_response.ok = True mock_response.status_code = 200 - mock_response.json.return_value = {"success": True, "output": {}} + mock_response.json.return_value = v2_execution_response() mock_response.headers.get.return_value = None mock_post.return_value = mock_response @@ -452,7 +508,7 @@ def test_execute_workflow_with_stream_and_selected_outputs(mock_post): call_args = mock_post.call_args request_body = call_args[1]["json"] - assert request_body["message"] == "test" + assert request_body["input"] == {"message": "test"} assert request_body["stream"] is True assert request_body["selectedOutputs"] == ["agent1.content", "agent2.content"] @@ -464,7 +520,7 @@ def test_execute_workflow_with_string_input(mock_post): mock_response = Mock() mock_response.ok = True mock_response.status_code = 200 - mock_response.json.return_value = {"success": True, "output": {}} + mock_response.json.return_value = v2_execution_response() mock_response.headers.get.return_value = None mock_post.return_value = mock_response @@ -474,7 +530,7 @@ def test_execute_workflow_with_string_input(mock_post): call_args = mock_post.call_args request_body = call_args[1]["json"] - assert request_body["input"] == "NVDA" + assert request_body["input"] == {"input": "NVDA"} assert "0" not in request_body # Should not spread string characters @@ -484,7 +540,7 @@ def test_execute_workflow_with_number_input(mock_post): mock_response = Mock() mock_response.ok = True mock_response.status_code = 200 - mock_response.json.return_value = {"success": True, "output": {}} + mock_response.json.return_value = v2_execution_response() mock_response.headers.get.return_value = None mock_post.return_value = mock_response @@ -494,7 +550,7 @@ def test_execute_workflow_with_number_input(mock_post): call_args = mock_post.call_args request_body = call_args[1]["json"] - assert request_body["input"] == 42 + assert request_body["input"] == {"input": 42} @patch('simstudio.requests.Session.post') @@ -503,7 +559,7 @@ def test_execute_workflow_with_list_input(mock_post): mock_response = Mock() mock_response.ok = True mock_response.status_code = 200 - mock_response.json.return_value = {"success": True, "output": {}} + mock_response.json.return_value = v2_execution_response() mock_response.headers.get.return_value = None mock_post.return_value = mock_response @@ -513,17 +569,16 @@ def test_execute_workflow_with_list_input(mock_post): call_args = mock_post.call_args request_body = call_args[1]["json"] - assert request_body["input"] == ["NVDA", "AAPL", "GOOG"] + assert request_body["input"] == {"input": ["NVDA", "AAPL", "GOOG"]} assert "0" not in request_body # Should not spread list @patch('simstudio.requests.Session.post') -def test_execute_workflow_with_dict_input_spreads_at_root(mock_post): - """Test execution with dict input spreads at root level.""" +def test_execute_workflow_with_dict_input_uses_v2_input_field(mock_post): mock_response = Mock() mock_response.ok = True mock_response.status_code = 200 - mock_response.json.return_value = {"success": True, "output": {}} + mock_response.json.return_value = v2_execution_response() mock_response.headers.get.return_value = None mock_post.return_value = mock_response @@ -533,6 +588,4 @@ def test_execute_workflow_with_dict_input_spreads_at_root(mock_post): call_args = mock_post.call_args request_body = call_args[1]["json"] - assert request_body["ticker"] == "NVDA" - assert request_body["quantity"] == 100 - assert "input" not in request_body # Should not wrap in input field + assert request_body["input"] == {"ticker": "NVDA", "quantity": 100} diff --git a/packages/ts-sdk/README.md b/packages/ts-sdk/README.md index 49adb82debc..2e2831ea93b 100644 --- a/packages/ts-sdk/README.md +++ b/packages/ts-sdk/README.md @@ -52,12 +52,12 @@ new SimStudioClient(config: SimStudioConfig) Execute a workflow with optional input data. ```typescript -// With object input (spread at root level of request body) +// With object input (sent as the v2 input object) const result = await client.executeWorkflow('workflow-id', { message: 'Hello, world!' }); -// With primitive input (wrapped as { input: value }) +// With primitive input (sent as { input: { input: value } }) const result = await client.executeWorkflow('workflow-id', 'NVDA'); // With options @@ -68,7 +68,7 @@ const result = await client.executeWorkflow('workflow-id', { message: 'Hello' }, **Parameters:** - `workflowId` (string): The ID of the workflow to execute -- `input` (any, optional): Input data to pass to the workflow. Objects are spread at the root level, primitives/arrays are wrapped in `{ input: value }`. File objects are automatically converted to base64. +- `input` (any, optional): Input data to pass to the workflow. Objects become the v2 `input` object; primitives and arrays become `{ input: value }` inside it. File objects are automatically converted to base64. - `options` (ExecutionOptions, optional): - `timeout` (number): Timeout in milliseconds (default: 30000) - `stream` (boolean): Enable streaming responses @@ -145,6 +145,16 @@ console.log('Execution status:', status.status); **Returns:** `Promise` +##### getJobStatus(jobId) + +Get the status of a job created through the legacy async execution endpoint. New integrations should use `getWorkflowExecution()` with an execution ID. + +```typescript +const status = await client.getJobStatus('legacy-job-id'); +``` + +**Returns:** `Promise` + ##### executeWithRetry(workflowId, input?, options?, retryOptions?) Execute a workflow with automatic retry on rate limit errors. diff --git a/packages/ts-sdk/src/index.test.ts b/packages/ts-sdk/src/index.test.ts index e59c3698f99..95137c9c23e 100644 --- a/packages/ts-sdk/src/index.test.ts +++ b/packages/ts-sdk/src/index.test.ts @@ -4,6 +4,19 @@ import { SimStudioClient, SimStudioError } from './index' const mockFetch = vi.fn() vi.stubGlobal('fetch', mockFetch) +function v2ExecutionResponse(output: unknown = {}) { + return { + data: { + executionId: 'execution-123', + workflowId: 'workflow-id', + status: 'completed', + output, + error: null, + durationMs: 10, + }, + } +} + describe('SimStudioClient', () => { let client: SimStudioClient @@ -100,11 +113,10 @@ describe('SimStudioClient', () => { ok: true, status: 202, json: vi.fn().mockResolvedValue({ - success: true, - executionId: 'execution-123', - statusUrl: 'https://test.sim.ai/api/workflows/workflow-id/executions/execution-123', - message: 'Workflow execution queued', - async: true, + data: { + executionId: 'execution-123', + statusUrl: 'https://test.sim.ai/api/v2/workflows/workflow-id/executions/execution-123', + }, }), headers: { get: vi.fn().mockReturnValue(null), @@ -121,14 +133,16 @@ describe('SimStudioClient', () => { expect(result).toHaveProperty('executionId', 'execution-123') expect(result).toHaveProperty( 'statusUrl', - 'https://test.sim.ai/api/workflows/workflow-id/executions/execution-123' + 'https://test.sim.ai/api/v2/workflows/workflow-id/executions/execution-123' ) expect(result).toHaveProperty('async', true) - // Verify headers were set correctly const calls = vi.mocked(mockFetch).mock.calls - expect(calls[0][1]?.headers).toMatchObject({ - 'X-Execution-Mode': 'async', + expect(calls[0][0]).toBe('https://test.sim.ai/api/v2/workflows/workflow-id/execute') + expect(calls[0][1]?.headers).not.toHaveProperty('X-Execution-Mode') + expect(JSON.parse(calls[0][1]?.body as string)).toEqual({ + input: { message: 'Hello' }, + async: true, }) }) @@ -136,11 +150,7 @@ describe('SimStudioClient', () => { const mockResponse = { ok: true, status: 200, - json: vi.fn().mockResolvedValue({ - success: true, - output: { result: 'completed' }, - logs: [], - }), + json: vi.fn().mockResolvedValue(v2ExecutionResponse({ result: 'completed' })), headers: { get: vi.fn().mockReturnValue(null), }, @@ -162,10 +172,7 @@ describe('SimStudioClient', () => { const mockResponse = { ok: true, status: 200, - json: vi.fn().mockResolvedValue({ - success: true, - output: {}, - }), + json: vi.fn().mockResolvedValue(v2ExecutionResponse()), headers: { get: vi.fn().mockReturnValue(null), }, @@ -179,15 +186,61 @@ describe('SimStudioClient', () => { }) }) - describe('getWorkflowExecution', () => { - it('should fetch execution status and outputs from the execution resource', async () => { + describe('getJobStatus', () => { + it('should fetch legacy job status with the correct endpoint', async () => { const mockResponse = { ok: true, json: vi.fn().mockResolvedValue({ - executionId: 'execution-123', - workflowId: 'workflow-123', + success: true, + taskId: 'task-123', status: 'completed', - finalOutput: { result: 'done' }, + metadata: { duration: 60000 }, + output: { result: 'done' }, + }), + headers: { + get: vi.fn().mockReturnValue(null), + }, + } + vi.mocked(mockFetch).mockResolvedValue(mockResponse as any) + + const result = await client.getJobStatus('task-123') + + expect(result).toHaveProperty('taskId', 'task-123') + expect(result).toHaveProperty('status', 'completed') + expect(result).toHaveProperty('output') + expect(vi.mocked(mockFetch).mock.calls[0][0]).toBe('https://test.sim.ai/api/jobs/task-123') + }) + + it('should handle legacy job not found errors', async () => { + const mockResponse = { + ok: false, + status: 404, + statusText: 'Not Found', + json: vi.fn().mockResolvedValue({ + error: 'Job not found', + code: 'JOB_NOT_FOUND', + }), + headers: { + get: vi.fn().mockReturnValue(null), + }, + } + vi.mocked(mockFetch).mockResolvedValue(mockResponse as any) + + await expect(client.getJobStatus('invalid-task')).rejects.toThrow('Job not found') + }) + }) + + describe('getWorkflowExecution', () => { + it('should fetch execution status and outputs from the v2 execution resource', async () => { + const mockResponse = { + ok: true, + json: vi.fn().mockResolvedValue({ + data: { + executionId: 'execution-123', + workflowId: 'workflow-123', + status: 'completed', + output: { result: 'done' }, + }, }), headers: { get: vi.fn().mockReturnValue(null), @@ -202,12 +255,11 @@ describe('SimStudioClient', () => { expect(result).toHaveProperty('executionId', 'execution-123') expect(result).toHaveProperty('status', 'completed') - expect(result).toHaveProperty('finalOutput') + expect(result).toHaveProperty('output') - // Verify correct endpoint was called const calls = vi.mocked(mockFetch).mock.calls expect(calls[0][0]).toBe( - 'https://test.sim.ai/api/workflows/workflow-123/executions/execution-123?includeOutput=true&selectedOutputs=agent.content' + 'https://test.sim.ai/api/v2/workflows/workflow-123/executions/execution-123?includeOutput=true&selectedOutputs=agent.content' ) }) @@ -217,8 +269,10 @@ describe('SimStudioClient', () => { status: 404, statusText: 'Not Found', json: vi.fn().mockResolvedValue({ - error: 'Execution not found', - code: 'EXECUTION_NOT_FOUND', + error: { + code: 'NOT_FOUND', + message: 'Execution not found', + }, }), headers: { get: vi.fn().mockReturnValue(null), @@ -240,10 +294,7 @@ describe('SimStudioClient', () => { const mockResponse = { ok: true, status: 200, - json: vi.fn().mockResolvedValue({ - success: true, - output: { result: 'success' }, - }), + json: vi.fn().mockResolvedValue(v2ExecutionResponse({ result: 'success' })), headers: { get: vi.fn().mockReturnValue(null), }, @@ -280,10 +331,7 @@ describe('SimStudioClient', () => { const successResponse = { ok: true, status: 200, - json: vi.fn().mockResolvedValue({ - success: true, - output: { result: 'success' }, - }), + json: vi.fn().mockResolvedValue(v2ExecutionResponse({ result: 'success' })), headers: { get: vi.fn().mockReturnValue(null), }, @@ -341,8 +389,10 @@ describe('SimStudioClient', () => { status: 500, statusText: 'Internal Server Error', json: vi.fn().mockResolvedValue({ - error: 'Server error', - code: 'INTERNAL_ERROR', + error: { + code: 'INTERNAL_ERROR', + message: 'Server error', + }, }), headers: { get: vi.fn().mockReturnValue(null), @@ -369,7 +419,7 @@ describe('SimStudioClient', () => { const mockResponse = { ok: true, status: 200, - json: vi.fn().mockResolvedValue({ success: true, output: {} }), + json: vi.fn().mockResolvedValue(v2ExecutionResponse()), headers: { get: vi.fn((header: string) => { if (header === 'x-ratelimit-limit') return '100' @@ -475,10 +525,7 @@ describe('SimStudioClient', () => { const mockResponse = { ok: true, status: 200, - json: vi.fn().mockResolvedValue({ - success: true, - output: {}, - }), + json: vi.fn().mockResolvedValue(v2ExecutionResponse()), headers: { get: vi.fn().mockReturnValue(null), }, @@ -495,7 +542,7 @@ describe('SimStudioClient', () => { const calls = vi.mocked(mockFetch).mock.calls const requestBody = JSON.parse(calls[0][1]?.body as string) - expect(requestBody).toHaveProperty('message', 'test') + expect(requestBody.input).toEqual({ message: 'test' }) expect(requestBody).toHaveProperty('stream', true) expect(requestBody).toHaveProperty('selectedOutputs') expect(requestBody.selectedOutputs).toEqual(['agent1.content', 'agent2.content']) @@ -507,10 +554,7 @@ describe('SimStudioClient', () => { const mockResponse = { ok: true, status: 200, - json: vi.fn().mockResolvedValue({ - success: true, - output: {}, - }), + json: vi.fn().mockResolvedValue(v2ExecutionResponse()), headers: { get: vi.fn().mockReturnValue(null), }, @@ -523,7 +567,7 @@ describe('SimStudioClient', () => { const calls = vi.mocked(mockFetch).mock.calls const requestBody = JSON.parse(calls[0][1]?.body as string) - expect(requestBody).toHaveProperty('input', 'NVDA') + expect(requestBody.input).toEqual({ input: 'NVDA' }) expect(requestBody).not.toHaveProperty('0') // Should not spread string characters }) @@ -531,10 +575,7 @@ describe('SimStudioClient', () => { const mockResponse = { ok: true, status: 200, - json: vi.fn().mockResolvedValue({ - success: true, - output: {}, - }), + json: vi.fn().mockResolvedValue(v2ExecutionResponse()), headers: { get: vi.fn().mockReturnValue(null), }, @@ -547,17 +588,14 @@ describe('SimStudioClient', () => { const calls = vi.mocked(mockFetch).mock.calls const requestBody = JSON.parse(calls[0][1]?.body as string) - expect(requestBody).toHaveProperty('input', 42) + expect(requestBody.input).toEqual({ input: 42 }) }) it('should wrap array input in input field', async () => { const mockResponse = { ok: true, status: 200, - json: vi.fn().mockResolvedValue({ - success: true, - output: {}, - }), + json: vi.fn().mockResolvedValue(v2ExecutionResponse()), headers: { get: vi.fn().mockReturnValue(null), }, @@ -570,8 +608,7 @@ describe('SimStudioClient', () => { const calls = vi.mocked(mockFetch).mock.calls const requestBody = JSON.parse(calls[0][1]?.body as string) - expect(requestBody).toHaveProperty('input') - expect(requestBody.input).toEqual(['NVDA', 'AAPL', 'GOOG']) + expect(requestBody.input).toEqual({ input: ['NVDA', 'AAPL', 'GOOG'] }) expect(requestBody).not.toHaveProperty('0') // Should not spread array }) @@ -579,10 +616,7 @@ describe('SimStudioClient', () => { const mockResponse = { ok: true, status: 200, - json: vi.fn().mockResolvedValue({ - success: true, - output: {}, - }), + json: vi.fn().mockResolvedValue(v2ExecutionResponse()), headers: { get: vi.fn().mockReturnValue(null), }, @@ -595,19 +629,14 @@ describe('SimStudioClient', () => { const calls = vi.mocked(mockFetch).mock.calls const requestBody = JSON.parse(calls[0][1]?.body as string) - expect(requestBody).toHaveProperty('ticker', 'NVDA') - expect(requestBody).toHaveProperty('quantity', 100) - expect(requestBody).not.toHaveProperty('input') // Should not wrap in input field + expect(requestBody.input).toEqual({ ticker: 'NVDA', quantity: 100 }) }) it('should handle null input as no input (empty body)', async () => { const mockResponse = { ok: true, status: 200, - json: vi.fn().mockResolvedValue({ - success: true, - output: {}, - }), + json: vi.fn().mockResolvedValue(v2ExecutionResponse()), headers: { get: vi.fn().mockReturnValue(null), }, @@ -620,8 +649,7 @@ describe('SimStudioClient', () => { const calls = vi.mocked(mockFetch).mock.calls const requestBody = JSON.parse(calls[0][1]?.body as string) - // null treated as "no input" - sends empty body (consistent with Python SDK) - expect(requestBody).toEqual({}) + expect(requestBody).toEqual({ input: {} }) }) }) }) diff --git a/packages/ts-sdk/src/index.ts b/packages/ts-sdk/src/index.ts index b38b7dfae3a..4d8777867f5 100644 --- a/packages/ts-sdk/src/index.ts +++ b/packages/ts-sdk/src/index.ts @@ -51,19 +51,32 @@ export interface AsyncExecutionResult { async: true } +export interface JobStatusResult { + taskId: string + status: string + metadata?: Record + output?: unknown + error?: string +} + +export interface WorkflowExecutionError { + code: string + message: string + details?: unknown +} + export interface WorkflowExecutionStatus { executionId: string workflowId: string status: 'queued' | 'pending' | 'running' | 'paused' | 'completed' | 'failed' | 'cancelled' - trigger: string - level: string - startedAt: string + trigger: string | null + startedAt: string | null endedAt: string | null - totalDurationMs: number | null + durationMs: number | null paused: Record | null cost: { total: number } | null - error: string | null - finalOutput: unknown | null + error: WorkflowExecutionError | null + output: unknown | null blockOutputs: Record | null } @@ -227,7 +240,7 @@ export class SimStudioClient { input?: any, options: ExecutionOptions = {} ): Promise { - const url = `${this.baseUrl}/api/workflows/${workflowId}/execute` + const url = `${this.baseUrl}/api/v2/workflows/${workflowId}/execute` const { timeout = 30000, stream, selectedOutputs, async } = options try { @@ -239,20 +252,18 @@ export class SimStudioClient { 'Content-Type': 'application/json', 'X-API-Key': this.apiKey, } - if (async) { - headers['X-Execution-Mode'] = 'async' - } - let jsonBody: any = {} + let workflowInput: any = {} if (input !== undefined && input !== null) { if (typeof input === 'object' && input !== null && !Array.isArray(input)) { - jsonBody = { ...input } + workflowInput = { ...input } } else { - jsonBody = { input } + workflowInput = { input } } } - jsonBody = await this.convertFilesToBase64(jsonBody) + workflowInput = await this.convertFilesToBase64(workflowInput) + const jsonBody: Record = { input: workflowInput } if (stream !== undefined) { jsonBody.stream = stream @@ -260,6 +271,9 @@ export class SimStudioClient { if (selectedOutputs !== undefined) { jsonBody.selectedOutputs = selectedOutputs } + if (async !== undefined) { + jsonBody.async = async + } const fetchPromise = fetch(url, { method: 'POST', @@ -281,16 +295,53 @@ export class SimStudioClient { } if (!response.ok) { - const errorData = (await response.json().catch(() => ({}))) as unknown as any + const errorData = (await response.json().catch(() => ({}))) as { + error?: { code?: string; message?: string } + } throw new SimStudioError( - errorData.error || `HTTP ${response.status}: ${response.statusText}`, - errorData.code, + errorData.error?.message || `HTTP ${response.status}: ${response.statusText}`, + errorData.error?.code, response.status ) } - const result = await response.json() - return result as WorkflowExecutionResult | AsyncExecutionResult + const result = (await response.json()) as { + data?: { + executionId: string + statusUrl?: string + status?: 'completed' | 'failed' | 'paused' | 'cancelled' + output?: unknown + error?: WorkflowExecutionError | null + durationMs?: number + } + } + if (!result.data) { + throw new SimStudioError('Invalid v2 workflow execution response', 'EXECUTION_ERROR') + } + + if (response.status === 202) { + if (!result.data.statusUrl) { + throw new SimStudioError('Invalid v2 async execution response', 'EXECUTION_ERROR') + } + return { + success: true, + executionId: result.data.executionId, + statusUrl: result.data.statusUrl, + message: 'Workflow execution queued', + async: true, + } + } + + return { + success: result.data.status !== 'failed', + output: result.data.output, + error: result.data.error?.message, + metadata: { + duration: result.data.durationMs, + executionId: result.data.executionId, + }, + totalDuration: result.data.durationMs, + } } catch (error: any) { if (error instanceof SimStudioError) { throw error @@ -322,7 +373,10 @@ export class SimStudioClient { }) if (!response.ok) { - const errorData = (await response.json().catch(() => ({}))) as unknown as any + const errorData = (await response.json().catch(() => ({}))) as { + error?: string + code?: string + } throw new SimStudioError( errorData.error || `HTTP ${response.status}: ${response.statusText}`, errorData.code, @@ -386,7 +440,44 @@ export class SimStudioClient { } /** - * Get a workflow execution's current status and optional outputs. + * Get the status of a legacy async job. + * @param taskId The job ID returned from legacy async execution + */ + async getJobStatus(taskId: string): Promise { + const url = `${this.baseUrl}/api/jobs/${taskId}` + + try { + const response = await fetch(url, { + method: 'GET', + headers: { + 'X-API-Key': this.apiKey, + }, + }) + + this.updateRateLimitInfo(response) + + if (!response.ok) { + const errorData = (await response.json().catch(() => ({}))) as unknown as any + throw new SimStudioError( + errorData.error || `HTTP ${response.status}: ${response.statusText}`, + errorData.code, + response.status + ) + } + + const result = await response.json() + return result as JobStatusResult + } catch (error: any) { + if (error instanceof SimStudioError) { + throw error + } + + throw new SimStudioError(describeError(error) || 'Failed to get job status', 'STATUS_ERROR') + } + } + + /** + * Get a workflow execution's current status and optional outputs from the v2 API. */ async getWorkflowExecution( workflowId: string, @@ -401,7 +492,7 @@ export class SimStudioClient { query.set('selectedOutputs', options.selectedOutputs.join(',')) } const queryString = query.toString() - const url = `${this.baseUrl}/api/workflows/${workflowId}/executions/${executionId}${queryString ? `?${queryString}` : ''}` + const url = `${this.baseUrl}/api/v2/workflows/${workflowId}/executions/${executionId}${queryString ? `?${queryString}` : ''}` try { const response = await fetch(url, { @@ -414,16 +505,21 @@ export class SimStudioClient { this.updateRateLimitInfo(response) if (!response.ok) { - const errorData = (await response.json().catch(() => ({}))) as unknown as any + const errorData = (await response.json().catch(() => ({}))) as { + error?: { code?: string; message?: string } + } throw new SimStudioError( - errorData.error || `HTTP ${response.status}: ${response.statusText}`, - errorData.code, + errorData.error?.message || `HTTP ${response.status}: ${response.statusText}`, + errorData.error?.code, response.status ) } - const result = await response.json() - return result as WorkflowExecutionStatus + const result = (await response.json()) as { data?: WorkflowExecutionStatus } + if (!result.data) { + throw new SimStudioError('Invalid v2 workflow execution response', 'STATUS_ERROR') + } + return result.data } catch (error: any) { if (error instanceof SimStudioError) { throw error From 8e2c0b9e42a4fe8adcb88d181f0e3da096599698 Mon Sep 17 00:00:00 2001 From: Theodore Li Date: Wed, 5 Aug 2026 14:07:01 -0700 Subject: [PATCH 3/8] fix(api): make execution polling resume-aware --- .../[executionId]/[contextId]/route.test.ts | 2 +- .../[executionId]/[contextId]/route.ts | 2 +- .../async-jobs/backends/trigger-dev.test.ts | 76 ++++++++++++---- .../core/async-jobs/backends/trigger-dev.ts | 22 ++++- .../executor/execution-status.test.ts | 61 ++++++++++--- .../workflows/executor/execution-status.ts | 87 ++++++++++++------- 6 files changed, 190 insertions(+), 60 deletions(-) diff --git a/apps/sim/app/api/resume/[workflowId]/[executionId]/[contextId]/route.test.ts b/apps/sim/app/api/resume/[workflowId]/[executionId]/[contextId]/route.test.ts index f63098a3e26..4a65a1fffaf 100644 --- a/apps/sim/app/api/resume/[workflowId]/[executionId]/[contextId]/route.test.ts +++ b/apps/sim/app/api/resume/[workflowId]/[executionId]/[contextId]/route.test.ts @@ -269,7 +269,7 @@ describe('POST /api/resume/[workflowId]/[executionId]/[contextId]', () => { 'resume-execution', expect.objectContaining({ resumeExecutionId: 'resume-execution-1' }), expect.objectContaining({ - jobId: 'resume-execution:resume-execution-1', + jobId: 'resume-execution:resume-entry-1', metadata: expect.objectContaining({ workflowId: WORKFLOW_ID }), }) ) diff --git a/apps/sim/app/api/resume/[workflowId]/[executionId]/[contextId]/route.ts b/apps/sim/app/api/resume/[workflowId]/[executionId]/[contextId]/route.ts index 0864ac89a6c..6aa7a8c18b5 100644 --- a/apps/sim/app/api/resume/[workflowId]/[executionId]/[contextId]/route.ts +++ b/apps/sim/app/api/resume/[workflowId]/[executionId]/[contextId]/route.ts @@ -327,7 +327,7 @@ export const POST = withRouteHandler( try { const jobQueue = await getJobQueue() queueJobId = await jobQueue.enqueue('resume-execution', resumePayload, { - jobId: `${RESUME_EXECUTION_JOB_ID_PREFIX}${enqueueResult.resumeExecutionId}`, + jobId: `${RESUME_EXECUTION_JOB_ID_PREFIX}${enqueueResult.resumeEntryId}`, metadata: { workflowId, workspaceId: workflow.workspaceId, userId }, }) logger.info('Enqueued async resume execution', { diff --git a/apps/sim/lib/core/async-jobs/backends/trigger-dev.test.ts b/apps/sim/lib/core/async-jobs/backends/trigger-dev.test.ts index 00642c49321..7e3577dab0a 100644 --- a/apps/sim/lib/core/async-jobs/backends/trigger-dev.test.ts +++ b/apps/sim/lib/core/async-jobs/backends/trigger-dev.test.ts @@ -3,22 +3,25 @@ */ import { beforeEach, describe, expect, it, vi } from 'vitest' -const { MockApiError, mockResolveTriggerRegion, mockTrigger } = vi.hoisted(() => { - class MockApiError extends Error { - constructor( - readonly status: number | undefined, - message: string - ) { - super(message) +const { MockApiError, mockListRuns, mockResolveTriggerRegion, mockRetrieveRun, mockTrigger } = + vi.hoisted(() => { + class MockApiError extends Error { + constructor( + readonly status: number | undefined, + message: string + ) { + super(message) + } } - } - return { - MockApiError, - mockResolveTriggerRegion: vi.fn(), - mockTrigger: vi.fn(), - } -}) + return { + MockApiError, + mockListRuns: vi.fn(), + mockResolveTriggerRegion: vi.fn(), + mockRetrieveRun: vi.fn(), + mockTrigger: vi.fn(), + } + }) vi.mock('@trigger.dev/core/v3', () => ({ taskContext: { isInsideTask: false }, @@ -28,7 +31,8 @@ vi.mock('@trigger.dev/sdk', () => ({ ApiError: MockApiError, runs: { cancel: vi.fn(), - retrieve: vi.fn(), + list: mockListRuns, + retrieve: mockRetrieveRun, }, tasks: { batchTriggerAndWait: vi.fn(), @@ -63,6 +67,7 @@ describe('TriggerDevJobQueue enqueue', () => { expect.objectContaining({ idempotencyKey: 'workflow:1', idempotencyKeyTTL: '14d', + tags: ['jobId:workflow:1'], }) ) }) @@ -113,3 +118,44 @@ describe('TriggerDevJobQueue enqueue', () => { expect(mockTrigger).not.toHaveBeenCalled() }) }) + +describe('TriggerDevJobQueue getJob', () => { + beforeEach(() => { + vi.clearAllMocks() + }) + + it('resolves a deterministic job ID through its Trigger.dev tag', async () => { + mockRetrieveRun + .mockRejectedValueOnce(new MockApiError(404, 'run not found')) + .mockResolvedValueOnce({ + id: 'run-1', + taskIdentifier: 'workflow-execution', + payload: { workflowId: 'workflow-1' }, + status: 'COMPLETED', + createdAt: '2026-08-05T12:00:00.000Z', + finishedAt: '2026-08-05T12:00:05.000Z', + attemptCount: 1, + output: { output: { answer: 42 } }, + }) + mockListRuns.mockReturnValueOnce( + (async function* () { + yield { id: 'run-1' } + })() + ) + const queue = new TriggerDevJobQueue() + + const job = await queue.getJob('workflow-execution:execution-1') + + expect(mockListRuns).toHaveBeenCalledWith({ + tag: 'jobId:workflow-execution:execution-1', + limit: 1, + }) + expect(mockRetrieveRun).toHaveBeenNthCalledWith(2, 'run-1') + expect(job).toMatchObject({ + id: 'workflow-execution:execution-1', + status: 'completed', + output: { output: { answer: 42 } }, + metadata: { workflowId: 'workflow-1' }, + }) + }) +}) diff --git a/apps/sim/lib/core/async-jobs/backends/trigger-dev.ts b/apps/sim/lib/core/async-jobs/backends/trigger-dev.ts index 12f9f15bc88..4059f3066dc 100644 --- a/apps/sim/lib/core/async-jobs/backends/trigger-dev.ts +++ b/apps/sim/lib/core/async-jobs/backends/trigger-dev.ts @@ -189,7 +189,26 @@ export class TriggerDevJobQueue implements JobQueueBackend { async getJob(jobId: string): Promise { try { - const run = await runs.retrieve(jobId) + let run: Awaited> + try { + run = await runs.retrieve(jobId) + } catch (error) { + const isNotFound = + (error instanceof Error && error.message.toLowerCase().includes('not found')) || + (error && typeof error === 'object' && 'status' in error && error.status === 404) + if (!isNotFound) throw error + + let runId: string | undefined + for await (const candidate of runs.list({ tag: `jobId:${jobId}`, limit: 1 })) { + runId = candidate.id + break + } + if (!runId) { + logger.debug('Job not found in trigger.dev', { jobId }) + return null + } + run = await runs.retrieve(runId) + } const payload = run.payload as Record const metadata: JobMetadata = { @@ -270,6 +289,7 @@ function buildTags(options?: EnqueueOptions): string[] { const tags: string[] = [] const meta = options?.metadata + if (options?.jobId) tags.push(`jobId:${options.jobId}`) if (meta?.workspaceId) tags.push(`workspaceId:${meta.workspaceId}`) if (meta?.workflowId) tags.push(`workflowId:${meta.workflowId}`) if (meta?.userId) tags.push(`userId:${meta.userId}`) diff --git a/apps/sim/lib/workflows/executor/execution-status.test.ts b/apps/sim/lib/workflows/executor/execution-status.test.ts index 02fb941d580..a986864ecee 100644 --- a/apps/sim/lib/workflows/executor/execution-status.test.ts +++ b/apps/sim/lib/workflows/executor/execution-status.test.ts @@ -30,7 +30,6 @@ describe('getWorkflowExecutionStatus queue projection', () => { beforeEach(() => { vi.clearAllMocks() resetDbChainMock() - queueTableRows(schemaMock.workflowExecutionLogs, []) }) it('projects a queued workflow job as an execution resource', async () => { @@ -57,8 +56,9 @@ describe('getWorkflowExecutionStatus queue projection', () => { expect(mockGetJob).toHaveBeenCalledWith('workflow-execution:execution-1') }) - it('uses the resume execution ID when the queued work is a resume attempt', async () => { - mockGetJob.mockResolvedValueOnce(null).mockResolvedValueOnce({ + it('uses the resume entry ID when the queued work is a resume attempt', async () => { + queueTableRows(schemaMock.resumeQueue, [{ id: 'resume-entry-1' }]) + mockGetJob.mockResolvedValueOnce({ status: 'processing', createdAt: new Date('2026-08-05T12:00:00.000Z'), startedAt: new Date('2026-08-05T12:00:01.000Z'), @@ -73,17 +73,56 @@ describe('getWorkflowExecutionStatus queue projection', () => { status: 'running', startedAt: '2026-08-05T12:00:01.000Z', }) - expect(mockGetJob).toHaveBeenNthCalledWith(2, 'resume-execution:execution-1') + expect(mockGetJob).toHaveBeenCalledWith('resume-execution:resume-entry-1') + }) + + it('projects an active resume ahead of the existing paused log', async () => { + queueTableRows(schemaMock.workflowExecutionLogs, [ + { + executionId: 'execution-1', + workflowId: 'workflow-1', + status: 'paused', + }, + ]) + queueTableRows(schemaMock.resumeQueue, [{ id: 'resume-entry-1' }]) + mockGetJob.mockResolvedValueOnce({ + status: 'pending', + createdAt: new Date('2026-08-05T12:00:00.000Z'), + metadata: { workflowId: 'workflow-1' }, + }) + + const status = await getWorkflowExecutionStatus(input) + + expect(status).toMatchObject({ + executionId: 'execution-1', + status: 'queued', + paused: null, + }) + }) + + it('returns completed queue output when requested', async () => { + mockGetJob.mockResolvedValueOnce({ + status: 'completed', + createdAt: new Date('2026-08-05T12:00:00.000Z'), + completedAt: new Date('2026-08-05T12:00:05.000Z'), + output: { output: { answer: 42 } }, + metadata: { workflowId: 'workflow-1' }, + }) + + const status = await getWorkflowExecutionStatus({ ...input, includeOutput: true }) + + expect(status).toMatchObject({ + status: 'completed', + finalOutput: { answer: 42 }, + }) }) it('does not expose a queue record belonging to another workflow', async () => { - mockGetJob - .mockResolvedValueOnce({ - status: 'pending', - createdAt: new Date('2026-08-05T12:00:00.000Z'), - metadata: { workflowId: 'workflow-2' }, - }) - .mockResolvedValueOnce(null) + mockGetJob.mockResolvedValueOnce({ + status: 'pending', + createdAt: new Date('2026-08-05T12:00:00.000Z'), + metadata: { workflowId: 'workflow-2' }, + }) await expect(getWorkflowExecutionStatus(input)).resolves.toBeNull() }) diff --git a/apps/sim/lib/workflows/executor/execution-status.ts b/apps/sim/lib/workflows/executor/execution-status.ts index 80f12184650..af2373ff231 100644 --- a/apps/sim/lib/workflows/executor/execution-status.ts +++ b/apps/sim/lib/workflows/executor/execution-status.ts @@ -1,8 +1,9 @@ import { db } from '@sim/db' -import { pausedExecutions, workflowExecutionLogs } from '@sim/db/schema' +import { pausedExecutions, resumeQueue, workflowExecutionLogs } from '@sim/db/schema' import { and, eq } from 'drizzle-orm' import type { WorkflowExecutionStatusResponse } from '@/lib/api/contracts/workflows' import { getJobQueue } from '@/lib/core/async-jobs' +import type { Job } from '@/lib/core/async-jobs/types' import { collectFunctionalBlockOutputs, type FunctionalExecutionDataSource, @@ -86,6 +87,38 @@ function extractError(executionData: unknown): string | null { return null } +function extractJobFinalOutput(output: unknown): unknown | null { + if (!output || typeof output !== 'object' || !('output' in output)) return null + return (output as Record).output ?? null +} + +function projectQueueJob( + job: Job, + input: Pick +): WorkflowExecutionStatusResponse { + const status: WorkflowExecutionStatusResponse['status'] = + job.status === 'pending' ? 'queued' : job.status === 'processing' ? 'running' : job.status + const startedAt = job.startedAt ?? job.createdAt + const endedAt = job.completedAt ?? null + + return { + executionId: input.executionId, + workflowId: input.workflowId, + status, + trigger: job.metadata.correlation?.triggerType ?? 'api', + level: status === 'failed' ? 'error' : 'info', + startedAt: startedAt.toISOString(), + endedAt: endedAt?.toISOString() ?? null, + totalDurationMs: endedAt ? endedAt.getTime() - startedAt.getTime() : null, + paused: null, + cost: null, + error: status === 'failed' ? (job.error ?? 'Execution failed') : null, + finalOutput: + input.includeOutput && status === 'completed' ? extractJobFinalOutput(job.output) : null, + blockOutputs: null, + } +} + export interface GetWorkflowExecutionStatusInput { workflowId: string executionId: string @@ -121,42 +154,34 @@ export async function getWorkflowExecutionStatus( ) .limit(1) - if (!logRow) { - const jobQueue = await getJobQueue() - const jobIds = [ - `${WORKFLOW_EXECUTION_JOB_ID_PREFIX}${executionId}`, - `${RESUME_EXECUTION_JOB_ID_PREFIX}${executionId}`, - ] + const [activeResume] = await db + .select({ id: resumeQueue.id }) + .from(resumeQueue) + .where( + and( + eq(resumeQueue.parentExecutionId, executionId), + eq(resumeQueue.newExecutionId, executionId), + eq(resumeQueue.status, 'claimed') + ) + ) + .limit(1) + + const queueJobIds = [ + ...(activeResume ? [`${RESUME_EXECUTION_JOB_ID_PREFIX}${activeResume.id}`] : []), + ...(!logRow ? [`${WORKFLOW_EXECUTION_JOB_ID_PREFIX}${executionId}`] : []), + ] - for (const jobId of jobIds) { + if (queueJobIds.length > 0) { + const jobQueue = await getJobQueue() + for (const jobId of queueJobIds) { const job = await jobQueue.getJob(jobId) if (!job || job.metadata.workflowId !== workflowId) continue - - const status: WorkflowExecutionStatusResponse['status'] = - job.status === 'pending' ? 'queued' : job.status === 'processing' ? 'running' : job.status - const startedAt = job.startedAt ?? job.createdAt - const endedAt = job.completedAt ?? null - - return { - executionId, - workflowId, - status, - trigger: job.metadata.correlation?.triggerType ?? 'api', - level: status === 'failed' ? 'error' : 'info', - startedAt: startedAt.toISOString(), - endedAt: endedAt?.toISOString() ?? null, - totalDurationMs: endedAt ? endedAt.getTime() - startedAt.getTime() : null, - paused: null, - cost: null, - error: status === 'failed' ? (job.error ?? 'Execution failed') : null, - finalOutput: null, - blockOutputs: null, - } + return projectQueueJob(job, { executionId, includeOutput, workflowId }) } - - return null } + if (!logRow) return null + const [pausedRow] = await db .select({ id: pausedExecutions.id, From 460a11e941dfbb68246e0a0c8f13bcafb65574ed Mon Sep 17 00:00:00 2001 From: Theodore Li Date: Wed, 5 Aug 2026 14:09:49 -0700 Subject: [PATCH 4/8] fix(ui): hide async examples for public workflows --- .../deploy-modal/components/api/api.tsx | 85 ++++++++++--------- 1 file changed, 44 insertions(+), 41 deletions(-) diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/components/deploy-modal/components/api/api.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/components/deploy-modal/components/api/api.tsx index eaa36f2aac0..aab37cbfa44 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/components/deploy-modal/components/api/api.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/components/deploy-modal/components/api/api.tsx @@ -256,6 +256,7 @@ while (true) { const getAsyncCommand = (): string => { if (!info) return '' + if (info.isPublicApi) throw new Error('Async execution requires an API key') const endpoint = getBaseEndpoint() const v2WorkflowPrefix = '/api/v2/workflows/' if (!endpoint.includes(v2WorkflowPrefix) || !endpoint.endsWith('/execute')) { @@ -538,49 +539,51 @@ console.log(limits);` /> -
-
- -
- - - - - - {copied.async ? 'Copied' : 'Copy'} - - - setAsyncExampleType(value as AsyncExampleType)} - align='end' - dropdownWidth={160} - /> + {!info.isPublicApi && ( +
+
+ +
+ + + + + + {copied.async ? 'Copied' : 'Copy'} + + + setAsyncExampleType(value as AsyncExampleType)} + align='end' + dropdownWidth={160} + /> +
+
- -
+ )}
) } From af162f8c37cbbf3238aabc97fab659f9d6bb5d2b Mon Sep 17 00:00:00 2001 From: Theodore Li Date: Wed, 5 Aug 2026 14:17:43 -0700 Subject: [PATCH 5/8] fix(api): bridge resume queue visibility lag --- .../executor/execution-status.test.ts | 29 +++++++++++++++++++ .../workflows/executor/execution-status.ts | 25 +++++++++++++++- 2 files changed, 53 insertions(+), 1 deletion(-) diff --git a/apps/sim/lib/workflows/executor/execution-status.test.ts b/apps/sim/lib/workflows/executor/execution-status.test.ts index a986864ecee..4b45ea3196b 100644 --- a/apps/sim/lib/workflows/executor/execution-status.test.ts +++ b/apps/sim/lib/workflows/executor/execution-status.test.ts @@ -100,6 +100,35 @@ describe('getWorkflowExecutionStatus queue projection', () => { }) }) + it('keeps an active resume queued while its background job is not yet visible', async () => { + queueTableRows(schemaMock.workflowExecutionLogs, [ + { + executionId: 'execution-1', + workflowId: 'workflow-1', + status: 'paused', + trigger: 'api', + }, + ]) + queueTableRows(schemaMock.resumeQueue, [ + { + id: 'resume-entry-1', + queuedAt: new Date('2026-08-05T12:00:00.000Z'), + claimedAt: new Date('2026-08-05T12:00:01.000Z'), + }, + ]) + mockGetJob.mockResolvedValueOnce(null) + + const status = await getWorkflowExecutionStatus(input) + + expect(status).toMatchObject({ + executionId: 'execution-1', + status: 'queued', + trigger: 'api', + startedAt: '2026-08-05T12:00:01.000Z', + paused: null, + }) + }) + it('returns completed queue output when requested', async () => { mockGetJob.mockResolvedValueOnce({ status: 'completed', diff --git a/apps/sim/lib/workflows/executor/execution-status.ts b/apps/sim/lib/workflows/executor/execution-status.ts index af2373ff231..3170f4cf359 100644 --- a/apps/sim/lib/workflows/executor/execution-status.ts +++ b/apps/sim/lib/workflows/executor/execution-status.ts @@ -155,7 +155,11 @@ export async function getWorkflowExecutionStatus( .limit(1) const [activeResume] = await db - .select({ id: resumeQueue.id }) + .select({ + id: resumeQueue.id, + queuedAt: resumeQueue.queuedAt, + claimedAt: resumeQueue.claimedAt, + }) .from(resumeQueue) .where( and( @@ -180,6 +184,25 @@ export async function getWorkflowExecutionStatus( } } + if (activeResume && logRow) { + const startedAt = activeResume.claimedAt ?? activeResume.queuedAt + return { + executionId, + workflowId, + status: 'queued', + trigger: logRow.trigger, + level: 'info', + startedAt: startedAt.toISOString(), + endedAt: null, + totalDurationMs: null, + paused: null, + cost: null, + error: null, + finalOutput: null, + blockOutputs: null, + } + } + if (!logRow) return null const [pausedRow] = await db From a4c11525d4f708a9b9ec3087549b76322bbf7097 Mon Sep 17 00:00:00 2001 From: Theodore Li Date: Wed, 5 Aug 2026 15:19:36 -0700 Subject: [PATCH 6/8] feat(api): add v2 workflow resume endpoint --- .../en/workflows/blocks/human-in-the-loop.mdx | 37 +- apps/docs/openapi-core.json | 3 +- apps/docs/openapi-v2-workflows.json | 167 ++++++++ apps/docs/openapi.json | 3 +- .../[executionId]/[contextId]/route.test.ts | 49 ++- .../[executionId]/[contextId]/route.ts | 353 +---------------- apps/sim/app/api/resume/resume-handler.ts | 374 ++++++++++++++++++ .../[executionId]/resume/route.test.ts | 170 ++++++++ .../executions/[executionId]/resume/route.ts | 147 +++++++ apps/sim/lib/api/contracts/v2/workflows.ts | 25 ++ 10 files changed, 971 insertions(+), 357 deletions(-) create mode 100644 apps/sim/app/api/resume/resume-handler.ts create mode 100644 apps/sim/app/api/v2/workflows/[id]/executions/[executionId]/resume/route.test.ts create mode 100644 apps/sim/app/api/v2/workflows/[id]/executions/[executionId]/resume/route.ts diff --git a/apps/docs/content/docs/en/workflows/blocks/human-in-the-loop.mdx b/apps/docs/content/docs/en/workflows/blocks/human-in-the-loop.mdx index de583ceb48b..3c5e403c33b 100644 --- a/apps/docs/content/docs/en/workflows/blocks/human-in-the-loop.mdx +++ b/apps/docs/content/docs/en/workflows/blocks/human-in-the-loop.mdx @@ -88,14 +88,15 @@ Access resume data in downstream blocks using ``. ### REST API - Programmatically resume workflows using the resume endpoint. The `contextId` is available from the block's `resumeEndpoint` output or from the `_resume` object in the paused execution response. + Programmatically resume workflows through the v2 execution resource. The `contextId` is available from the block's `resumeEndpoint` output or from the `_resume` object in the paused execution response. ```bash - POST /api/resume/{workflowId}/{executionId}/{contextId} + POST /api/v2/workflows/{workflowId}/executions/{executionId}/resume Content-Type: application/json X-API-Key: your-api-key { + "contextId": "", "input": { "approved": true, "comments": "Looks good to proceed" @@ -109,11 +110,16 @@ Access resume data in downstream blocks using ``. ```json { - "success": true, - "status": "completed", - "executionId": "", - "output": { ... }, - "metadata": { "duration": 1234, "startTime": "...", "endTime": "..." } + "data": { + "executionId": "", + "workflowId": "", + "status": "completed", + "output": { ... }, + "error": null, + "startedAt": "...", + "endedAt": "...", + "durationMs": 1234 + } } ``` @@ -125,11 +131,10 @@ Access resume data in downstream blocks using ``. ```json { - "success": true, - "async": true, - "executionId": "", - "message": "Resume execution queued", - "statusUrl": "/api/v2/workflows//executions/" + "data": { + "executionId": "", + "statusUrl": "/api/v2/workflows//executions/" + } } ``` @@ -143,6 +148,14 @@ Access resume data in downstream blocks using ``. ``` Returns the execution status and, when completed, the full workflow output. + + The legacy endpoint remains available without behavior changes for existing integrations: + + ```bash + POST /api/resume/{workflowId}/{executionId}/{contextId} + ``` + + Its async response continues to expose `jobId` and the legacy `/api/jobs/{jobId}` polling URL. To check on a paused execution's pause points and resume links: diff --git a/apps/docs/openapi-core.json b/apps/docs/openapi-core.json index 97da460bba4..ecde7d1730f 100644 --- a/apps/docs/openapi-core.json +++ b/apps/docs/openapi-core.json @@ -914,9 +914,10 @@ "example": { "success": true, "async": true, + "jobId": "job_4a3b2c1d0e", "executionId": "f0b3d8c2-7e5a-4b9d-8c1f-6a4e2d0b9c58", "message": "Resume execution queued", - "statusUrl": "https://www.sim.ai/api/v2/workflows/81f661e1-d704-4861-b5c1-5bb3cf57e6a7/executions/f0b3d8c2-7e5a-4b9d-8c1f-6a4e2d0b9c58" + "statusUrl": "https://www.sim.ai/api/jobs/job_4a3b2c1d0e" } } } diff --git a/apps/docs/openapi-v2-workflows.json b/apps/docs/openapi-v2-workflows.json index 22fc0bae059..00d943e77a7 100644 --- a/apps/docs/openapi-v2-workflows.json +++ b/apps/docs/openapi-v2-workflows.json @@ -1536,6 +1536,173 @@ } } }, + "/api/v2/workflows/{id}/executions/{executionId}/resume": { + "post": { + "operationId": "resumeWorkflowExecutionV2", + "summary": "Resume a workflow execution", + "description": "Resumes one human-in-the-loop pause context on the parent execution. The resumed attempt receives a new execution ID. Sync attempts return the execution resource, stream attempts return Server-Sent Events, and async or serialized attempts return a 202 receipt whose `statusUrl` is the v2 execution resource.", + "tags": ["Workflows"], + "security": [ + { + "apiKey": [] + } + ], + "parameters": [ + { + "$ref": "#/components/parameters/WorkflowId" + }, + { + "name": "executionId", + "in": "path", + "required": true, + "description": "The execution ID of the paused parent run.", + "schema": { + "type": "string", + "minLength": 1 + } + } + ], + "requestBody": { + "required": true, + "description": "The pause context to resume and its optional input. Bodies over 10 MB and unknown keys are rejected.", + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": false, + "required": ["contextId"], + "properties": { + "contextId": { + "type": "string", + "minLength": 1, + "description": "The context ID of the human-in-the-loop pause point." + }, + "input": { + "description": "Input supplied to the paused block." + } + } + }, + "example": { + "contextId": "ctx_123", + "input": { + "approved": true, + "comments": "Looks good to proceed" + } + } + } + } + }, + "responses": { + "200": { + "description": "The completed, failed, paused, or cancelled resume execution resource.", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": ["data"], + "properties": { + "data": { + "$ref": "#/components/schemas/ExecutionResource" + } + } + }, + "example": { + "data": { + "executionId": "resume_exec_1", + "workflowId": "wf_123", + "status": "completed", + "output": { + "result": "approved" + }, + "error": null, + "durationMs": 420 + } + } + } + } + }, + "202": { + "description": "The resume is queued. Poll `statusUrl` using the returned resume execution ID.", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": ["data"], + "properties": { + "data": { + "type": "object", + "required": ["executionId", "statusUrl"], + "properties": { + "executionId": { + "type": "string" + }, + "statusUrl": { + "type": "string" + }, + "queuePosition": { + "type": "integer", + "minimum": 1 + } + } + } + } + }, + "example": { + "data": { + "executionId": "resume_exec_1", + "statusUrl": "https://www.sim.ai/api/v2/workflows/wf_123/executions/resume_exec_1" + } + } + } + } + }, + "400": { + "$ref": "#/components/responses/BadRequest" + }, + "401": { + "$ref": "#/components/responses/Unauthorized" + }, + "403": { + "$ref": "#/components/responses/Forbidden" + }, + "404": { + "$ref": "#/components/responses/NotFound" + }, + "409": { + "description": "The pause context cannot be resumed in its current state.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "413": { + "$ref": "#/components/responses/PayloadTooLarge" + }, + "423": { + "$ref": "#/components/responses/Locked" + }, + "429": { + "$ref": "#/components/responses/RateLimited" + }, + "500": { + "$ref": "#/components/responses/InternalError" + }, + "503": { + "description": "Resume execution infrastructure temporarily unavailable.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + } + } + } + }, "/api/v2/workflows/{id}/executions/{executionId}/cancel": { "post": { "operationId": "cancelExecutionV2", diff --git a/apps/docs/openapi.json b/apps/docs/openapi.json index 21e6bfbb391..6e81a450470 100644 --- a/apps/docs/openapi.json +++ b/apps/docs/openapi.json @@ -874,9 +874,10 @@ "example": { "success": true, "async": true, + "jobId": "job_4a3b2c1d0e", "executionId": "f0b3d8c2-7e5a-4b9d-8c1f-6a4e2d0b9c58", "message": "Resume execution queued", - "statusUrl": "https://www.sim.ai/api/v2/workflows/81f661e1-d704-4861-b5c1-5bb3cf57e6a7/executions/f0b3d8c2-7e5a-4b9d-8c1f-6a4e2d0b9c58" + "statusUrl": "https://www.sim.ai/api/jobs/job_4a3b2c1d0e" } } } diff --git a/apps/sim/app/api/resume/[workflowId]/[executionId]/[contextId]/route.test.ts b/apps/sim/app/api/resume/[workflowId]/[executionId]/[contextId]/route.test.ts index 4a65a1fffaf..9ba0d765a7e 100644 --- a/apps/sim/app/api/resume/[workflowId]/[executionId]/[contextId]/route.test.ts +++ b/apps/sim/app/api/resume/[workflowId]/[executionId]/[contextId]/route.test.ts @@ -58,6 +58,7 @@ vi.mock('@/lib/workflows/executor/human-in-the-loop-manager', () => ({ })) import { GET, POST } from '@/app/api/resume/[workflowId]/[executionId]/[contextId]/route' +import { handleResumeExecution } from '@/app/api/resume/resume-handler' const WORKFLOW_ID = 'workflow-1' const EXECUTION_ID = 'execution-1' @@ -240,7 +241,7 @@ describe('POST /api/resume/[workflowId]/[executionId]/[contextId]', () => { }) }) - it('returns the resume execution ID as the only public async polling handle', async () => { + it('preserves the legacy async job polling response', async () => { mockGetPausedExecutionDetail.mockResolvedValueOnce( createPausedExecution({ executionMode: 'async' }) ) @@ -257,6 +258,52 @@ describe('POST /api/resume/[workflowId]/[executionId]/[contextId]', () => { const response = await POST(request, context) + expect(response.status).toBe(202) + await expect(response.json()).resolves.toEqual({ + success: true, + async: true, + jobId: 'resume-execution:resume-execution-1', + executionId: 'resume-execution-1', + message: 'Resume execution queued', + statusUrl: 'https://test.sim.ai/api/jobs/resume-execution:resume-execution-1', + }) + expect(mockEnqueueResume).toHaveBeenCalledWith( + 'resume-execution', + expect.objectContaining({ resumeExecutionId: 'resume-execution-1' }), + expect.objectContaining({ + metadata: expect.objectContaining({ workflowId: WORKFLOW_ID }), + }) + ) + expect(mockEnqueueResume.mock.calls[0]?.[2]).not.toHaveProperty('jobId') + }) + + it('uses deterministic dispatch and execution polling for the v2 surface', async () => { + mockGetPausedExecutionDetail.mockResolvedValueOnce( + createPausedExecution({ executionMode: 'async' }) + ) + mockEnqueueOrStartResume.mockResolvedValueOnce({ + status: 'started', + resumeExecutionId: 'resume-execution-1', + resumeEntryId: 'resume-entry-1', + pausedExecution: { id: 'paused-execution-1' }, + contextId: CONTEXT_ID, + resumeInput: { approved: true }, + userId: 'current-api-key-user', + }) + const { request } = makeRequest() + + const response = await handleResumeExecution({ + request, + workflowId: WORKFLOW_ID, + executionId: EXECUTION_ID, + contextId: CONTEXT_ID, + workspaceId: WORKSPACE_ID, + userId: 'current-api-key-user', + resumeInput: { approved: true }, + isApiCaller: true, + pollingSurface: 'v2', + }) + expect(response.status).toBe(202) await expect(response.json()).resolves.toEqual({ success: true, diff --git a/apps/sim/app/api/resume/[workflowId]/[executionId]/[contextId]/route.ts b/apps/sim/app/api/resume/[workflowId]/[executionId]/[contextId]/route.ts index 6aa7a8c18b5..80af4959ff9 100644 --- a/apps/sim/app/api/resume/[workflowId]/[executionId]/[contextId]/route.ts +++ b/apps/sim/app/api/resume/[workflowId]/[executionId]/[contextId]/route.ts @@ -1,107 +1,19 @@ -import { createLogger } from '@sim/logger' -import { toError } from '@sim/utils/errors' -import { generateId } from '@sim/utils/id' -import { isRecordLike } from '@sim/utils/object' -import { type NextRequest, NextResponse } from 'next/server' +import type { NextRequest } from 'next/server' +import { NextResponse } from 'next/server' import { getPauseContextDetailContract, resumeWorkflowExecutionContextContract, } from '@/lib/api/contracts/workflows' import { parseRequest } from '@/lib/api/server' import { AuthType } from '@/lib/auth/hybrid' -import { - assertBillingAttributionSnapshot, - type BillingAttributionSnapshot, -} from '@/lib/billing/core/billing-attribution' -import { getJobQueue } from '@/lib/core/async-jobs' -import { generateRequestId } from '@/lib/core/utils/request' -import { SSE_HEADERS } from '@/lib/core/utils/sse' -import { getBaseUrl } from '@/lib/core/utils/urls' import { withRouteHandler } from '@/lib/core/utils/with-route-handler' -import { preprocessExecution } from '@/lib/execution/preprocessing' -import { RESUME_EXECUTION_JOB_ID_PREFIX } from '@/lib/workflows/executor/enqueue-execution' import { PauseResumeManager } from '@/lib/workflows/executor/human-in-the-loop-manager' -import { - agentStreamProtocolResponseHeaders, - createStreamingResponse, -} from '@/lib/workflows/streaming/streaming' +import { handleResumeExecution } from '@/app/api/resume/resume-handler' import { validateWorkflowAccess } from '@/app/api/workflows/middleware' -import type { ResumeExecutionPayload } from '@/background/resume-execution' -import { ExecutionSnapshot } from '@/executor/execution/snapshot' - -const logger = createLogger('WorkflowResumeAPI') export const runtime = 'nodejs' export const dynamic = 'force-dynamic' -const INVALID_PAUSED_SNAPSHOT_ERROR = 'Paused execution snapshot is invalid' -const INVALID_PAUSED_ATTRIBUTION_ERROR = - 'Paused execution billing attribution is missing or invalid' -const PAUSED_EXECUTION_BINDING_ERROR = - 'Paused execution snapshot does not match the requested workflow or execution' -const PAUSED_ATTRIBUTION_BINDING_ERROR = - 'Paused execution billing attribution does not match its workspace or actor' - -interface PausedExecutionSnapshotSource { - workflowId: string - executionId: string - executionSnapshot: unknown -} - -interface PausedExecutionSnapshotBinding { - snapshot: ExecutionSnapshot - billingAttribution: BillingAttributionSnapshot -} - -function loadPausedExecutionSnapshot( - pausedExecution: PausedExecutionSnapshotSource, - expected: { workflowId: string; executionId: string; workspaceId: string } -): PausedExecutionSnapshotBinding { - if ( - !isRecordLike(pausedExecution.executionSnapshot) || - typeof pausedExecution.executionSnapshot.snapshot !== 'string' - ) { - throw new Error(INVALID_PAUSED_SNAPSHOT_ERROR) - } - - let snapshot: ExecutionSnapshot - try { - snapshot = ExecutionSnapshot.fromJSON(pausedExecution.executionSnapshot.snapshot) - } catch { - throw new Error(INVALID_PAUSED_SNAPSHOT_ERROR) - } - - if (!isRecordLike(snapshot.metadata)) { - throw new Error(INVALID_PAUSED_SNAPSHOT_ERROR) - } - - let billingAttribution: BillingAttributionSnapshot - try { - billingAttribution = assertBillingAttributionSnapshot(snapshot.metadata.billingAttribution) - } catch { - throw new Error(INVALID_PAUSED_ATTRIBUTION_ERROR) - } - - if ( - pausedExecution.workflowId !== expected.workflowId || - pausedExecution.executionId !== expected.executionId || - snapshot.metadata.workflowId !== expected.workflowId || - snapshot.metadata.executionId !== expected.executionId - ) { - throw new Error(PAUSED_EXECUTION_BINDING_ERROR) - } - - if ( - snapshot.metadata.workspaceId !== expected.workspaceId || - billingAttribution.workspaceId !== expected.workspaceId || - snapshot.metadata.userId !== billingAttribution.actorUserId - ) { - throw new Error(PAUSED_ATTRIBUTION_BINDING_ERROR) - } - - return { snapshot, billingAttribution } -} - export const POST = withRouteHandler( async ( request: NextRequest, @@ -118,11 +30,9 @@ export const POST = withRouteHandler( const parsed = await parseRequest(resumeWorkflowExecutionContextContract, request, context) if (!parsed.success) return parsed.response const { workflowId, executionId, contextId } = parsed.data.params - const requestId = generateRequestId() const workflow = access.workflow if (!workflow?.workspaceId) { - logger.error(`[${requestId}] Authorized workflow has no workspace`, { workflowId }) return NextResponse.json({ error: 'Workflow has no associated workspace' }, { status: 500 }) } const userId = access.auth?.userId @@ -130,269 +40,28 @@ export const POST = withRouteHandler( return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) } - const pausedExecution = await PauseResumeManager.getPausedExecutionDetail({ - workflowId, - executionId, - }) - if (!pausedExecution) { - return NextResponse.json({ error: 'Paused execution not found' }, { status: 404 }) - } - - let snapshotBinding: PausedExecutionSnapshotBinding - try { - snapshotBinding = loadPausedExecutionSnapshot(pausedExecution, { - workflowId, - executionId, - workspaceId: workflow.workspaceId, - }) - } catch (error) { - const message = toError(error).message - logger.error(`[${requestId}] Failed to validate paused execution snapshot`, { - workflowId, - executionId, - error: message, - }) - return NextResponse.json({ error: message }, { status: 500 }) - } - - const { snapshot: persistedSnapshot, billingAttribution } = snapshotBinding - let payload: unknown = {} try { payload = await request.json() } catch { payload = {} } - const resumeInput = typeof payload === 'object' && payload !== null && 'input' in payload ? payload.input : (payload ?? {}) - const resumeExecutionId = generateId() - logger.info(`[${requestId}] Preprocessing resume execution`, { + return handleResumeExecution({ + request, workflowId, - parentExecutionId: executionId, - resumeExecutionId, - userId, - actorUserId: billingAttribution.actorUserId, - }) - - /** - * This preflight gives synchronous callers current block/usage feedback - * without reserving under a throwaway id. The claimed resume reruns every - * gate and reserves atomically under its persisted resume execution id. - */ - const preprocessResult = await preprocessExecution({ - workflowId, - userId, - triggerType: 'manual', - executionId: resumeExecutionId, - requestId, - checkRateLimit: false, - checkDeployment: false, - skipConcurrencyReservation: true, - logPreprocessingErrors: false, + executionId, + contextId, workspaceId: workflow.workspaceId, - billingAttribution, - }) - - if (!preprocessResult.success) { - logger.warn(`[${requestId}] Preprocessing failed for resume`, { - workflowId, - parentExecutionId: executionId, - error: preprocessResult.error?.message, - statusCode: preprocessResult.error?.statusCode, - }) - - return NextResponse.json( - { - error: - preprocessResult.error?.message || - 'Failed to validate resume execution. Please try again.', - }, - { status: preprocessResult.error?.statusCode || 400 } - ) - } - - logger.info(`[${requestId}] Preprocessing passed, proceeding with resume`, { - workflowId, - parentExecutionId: executionId, - resumeExecutionId, - actorUserId: preprocessResult.actorUserId, + userId, + resumeInput, + isApiCaller: access.auth?.authType === AuthType.API_KEY, + pollingSurface: 'legacy', }) - - try { - const enqueueResult = await PauseResumeManager.enqueueOrStartResume({ - executionId, - workflowId, - contextId, - resumeInput, - userId, - allowedPauseKinds: ['human'], - }) - - if (enqueueResult.status === 'queued') { - return NextResponse.json({ - status: 'queued', - executionId: enqueueResult.resumeExecutionId, - queuePosition: enqueueResult.queuePosition, - message: 'Resume queued. It will run after current resumes finish.', - }) - } - - const resumeArgs = { - resumeEntryId: enqueueResult.resumeEntryId, - resumeExecutionId: enqueueResult.resumeExecutionId, - pausedExecution: enqueueResult.pausedExecution, - contextId: enqueueResult.contextId, - resumeInput: enqueueResult.resumeInput, - userId: enqueueResult.userId, - } - - const isApiCaller = access.auth?.authType === AuthType.API_KEY - const executionMode = isApiCaller - ? (persistedSnapshot.metadata.executionMode ?? 'sync') - : undefined - const includeThinking = persistedSnapshot.metadata.includeThinking === true - const includeToolCalls = persistedSnapshot.metadata.includeToolCalls === true - - if (isApiCaller && executionMode === 'stream') { - const stream = await createStreamingResponse({ - requestId, - streamConfig: { - selectedOutputs: persistedSnapshot.selectedOutputs, - timeoutMs: preprocessResult.executionTimeout?.sync, - includeThinking, - includeToolCalls, - }, - executionId: enqueueResult.resumeExecutionId, - workspaceId: workflow.workspaceId || undefined, - workflowId, - userId: enqueueResult.userId, - allowLargeValueWorkflowScope: true, - requestSignal: request.signal, - requestHeaders: request.headers, - executeFn: async ({ onStream, onBlockComplete, abortSignal }) => - PauseResumeManager.startResumeExecution({ - ...resumeArgs, - onStream, - onBlockComplete, - abortSignal, - }), - }) - - return new NextResponse(stream, { - headers: { - ...SSE_HEADERS, - // Echo the negotiated stream protocol (same as the public chat route). - ...agentStreamProtocolResponseHeaders({ requestHeaders: request.headers }), - 'X-Execution-Id': enqueueResult.resumeExecutionId, - }, - }) - } - - if (isApiCaller && executionMode === 'sync') { - const result = await PauseResumeManager.startResumeExecution(resumeArgs) - - return NextResponse.json({ - success: result.success, - status: result.status ?? (result.success ? 'completed' : 'failed'), - executionId: enqueueResult.resumeExecutionId, - output: result.output, - error: result.error, - metadata: result.metadata - ? { - duration: result.metadata.duration, - startTime: result.metadata.startTime, - endTime: result.metadata.endTime, - } - : undefined, - }) - } - - if (isApiCaller && executionMode === 'async') { - const resumePayload: ResumeExecutionPayload = { - resumeEntryId: enqueueResult.resumeEntryId, - resumeExecutionId: enqueueResult.resumeExecutionId, - pausedExecutionId: enqueueResult.pausedExecution.id, - contextId: enqueueResult.contextId, - resumeInput: enqueueResult.resumeInput, - userId: enqueueResult.userId, - workflowId, - parentExecutionId: executionId, - } - - let queueJobId: string - try { - const jobQueue = await getJobQueue() - queueJobId = await jobQueue.enqueue('resume-execution', resumePayload, { - jobId: `${RESUME_EXECUTION_JOB_ID_PREFIX}${enqueueResult.resumeEntryId}`, - metadata: { workflowId, workspaceId: workflow.workspaceId, userId }, - }) - logger.info('Enqueued async resume execution', { - jobId: queueJobId, - resumeExecutionId: enqueueResult.resumeExecutionId, - }) - } catch (dispatchError) { - logger.error('Failed to dispatch async resume execution', { - error: toError(dispatchError).message, - resumeExecutionId: enqueueResult.resumeExecutionId, - }) - await PauseResumeManager.markResumeAttemptFailed({ - resumeEntryId: enqueueResult.resumeEntryId, - pausedExecutionId: enqueueResult.pausedExecution.id, - parentExecutionId: executionId, - contextId: enqueueResult.contextId, - failureReason: 'Failed to queue async resume execution', - }) - await PauseResumeManager.processQueuedResumes(executionId, workflowId) - return NextResponse.json( - { error: 'Failed to queue resume execution. Please try again.' }, - { status: 503 } - ) - } - - return NextResponse.json( - { - success: true, - async: true, - executionId: enqueueResult.resumeExecutionId, - message: 'Resume execution queued', - statusUrl: `${getBaseUrl()}/api/v2/workflows/${workflowId}/executions/${enqueueResult.resumeExecutionId}`, - }, - { status: 202 } - ) - } - - PauseResumeManager.startResumeExecution(resumeArgs).catch((error) => { - logger.error('Failed to start resume execution', { - workflowId, - parentExecutionId: executionId, - resumeExecutionId: enqueueResult.resumeExecutionId, - error, - }) - }) - - return NextResponse.json({ - status: 'started', - executionId: enqueueResult.resumeExecutionId, - message: 'Resume execution started.', - }) - } catch (error) { - logger.error('Resume request failed', { - workflowId, - executionId, - contextId, - error, - }) - const statusCode = - isRecordLike(error) && typeof error.statusCode === 'number' ? error.statusCode : 400 - return NextResponse.json( - { error: toError(error).message || 'Failed to queue resume request' }, - { status: statusCode } - ) - } } ) diff --git a/apps/sim/app/api/resume/resume-handler.ts b/apps/sim/app/api/resume/resume-handler.ts new file mode 100644 index 00000000000..ad148ff3d74 --- /dev/null +++ b/apps/sim/app/api/resume/resume-handler.ts @@ -0,0 +1,374 @@ +import { createLogger } from '@sim/logger' +import { toError } from '@sim/utils/errors' +import { generateId } from '@sim/utils/id' +import { isRecordLike } from '@sim/utils/object' +import { type NextRequest, NextResponse } from 'next/server' +import { + assertBillingAttributionSnapshot, + type BillingAttributionSnapshot, +} from '@/lib/billing/core/billing-attribution' +import { getJobQueue } from '@/lib/core/async-jobs' +import { generateRequestId } from '@/lib/core/utils/request' +import { SSE_HEADERS } from '@/lib/core/utils/sse' +import { getBaseUrl } from '@/lib/core/utils/urls' +import { preprocessExecution } from '@/lib/execution/preprocessing' +import { RESUME_EXECUTION_JOB_ID_PREFIX } from '@/lib/workflows/executor/enqueue-execution' +import { PauseResumeManager } from '@/lib/workflows/executor/human-in-the-loop-manager' +import { + agentStreamProtocolResponseHeaders, + createStreamingResponse, +} from '@/lib/workflows/streaming/streaming' +import type { ResumeExecutionPayload } from '@/background/resume-execution' +import { ExecutionSnapshot } from '@/executor/execution/snapshot' + +const logger = createLogger('WorkflowResumeAPI') + +const INVALID_PAUSED_SNAPSHOT_ERROR = 'Paused execution snapshot is invalid' +const INVALID_PAUSED_ATTRIBUTION_ERROR = + 'Paused execution billing attribution is missing or invalid' +const PAUSED_EXECUTION_BINDING_ERROR = + 'Paused execution snapshot does not match the requested workflow or execution' +const PAUSED_ATTRIBUTION_BINDING_ERROR = + 'Paused execution billing attribution does not match its workspace or actor' + +interface PausedExecutionSnapshotSource { + workflowId: string + executionId: string + executionSnapshot: unknown +} + +interface PausedExecutionSnapshotBinding { + snapshot: ExecutionSnapshot + billingAttribution: BillingAttributionSnapshot +} + +interface HandleResumeExecutionOptions { + request: NextRequest + workflowId: string + executionId: string + contextId: string + workspaceId: string + userId: string + resumeInput: unknown + isApiCaller: boolean + pollingSurface: 'legacy' | 'v2' +} + +function loadPausedExecutionSnapshot( + pausedExecution: PausedExecutionSnapshotSource, + expected: { workflowId: string; executionId: string; workspaceId: string } +): PausedExecutionSnapshotBinding { + if ( + !isRecordLike(pausedExecution.executionSnapshot) || + typeof pausedExecution.executionSnapshot.snapshot !== 'string' + ) { + throw new Error(INVALID_PAUSED_SNAPSHOT_ERROR) + } + + let snapshot: ExecutionSnapshot + try { + snapshot = ExecutionSnapshot.fromJSON(pausedExecution.executionSnapshot.snapshot) + } catch { + throw new Error(INVALID_PAUSED_SNAPSHOT_ERROR) + } + + if (!isRecordLike(snapshot.metadata)) { + throw new Error(INVALID_PAUSED_SNAPSHOT_ERROR) + } + + let billingAttribution: BillingAttributionSnapshot + try { + billingAttribution = assertBillingAttributionSnapshot(snapshot.metadata.billingAttribution) + } catch { + throw new Error(INVALID_PAUSED_ATTRIBUTION_ERROR) + } + + if ( + pausedExecution.workflowId !== expected.workflowId || + pausedExecution.executionId !== expected.executionId || + snapshot.metadata.workflowId !== expected.workflowId || + snapshot.metadata.executionId !== expected.executionId + ) { + throw new Error(PAUSED_EXECUTION_BINDING_ERROR) + } + + if ( + snapshot.metadata.workspaceId !== expected.workspaceId || + billingAttribution.workspaceId !== expected.workspaceId || + snapshot.metadata.userId !== billingAttribution.actorUserId + ) { + throw new Error(PAUSED_ATTRIBUTION_BINDING_ERROR) + } + + return { snapshot, billingAttribution } +} + +/** Executes the shared resume flow while preserving each API surface's polling contract. */ +export async function handleResumeExecution({ + request, + workflowId, + executionId, + contextId, + workspaceId, + userId, + resumeInput, + isApiCaller, + pollingSurface, +}: HandleResumeExecutionOptions): Promise { + const requestId = generateRequestId() + const pausedExecution = await PauseResumeManager.getPausedExecutionDetail({ + workflowId, + executionId, + }) + if (!pausedExecution) { + return NextResponse.json({ error: 'Paused execution not found' }, { status: 404 }) + } + + let snapshotBinding: PausedExecutionSnapshotBinding + try { + snapshotBinding = loadPausedExecutionSnapshot(pausedExecution, { + workflowId, + executionId, + workspaceId, + }) + } catch (error) { + const message = toError(error).message + logger.error(`[${requestId}] Failed to validate paused execution snapshot`, { + workflowId, + executionId, + error: message, + }) + return NextResponse.json({ error: message }, { status: 500 }) + } + + const { snapshot: persistedSnapshot, billingAttribution } = snapshotBinding + const resumeExecutionId = generateId() + + logger.info(`[${requestId}] Preprocessing resume execution`, { + workflowId, + parentExecutionId: executionId, + resumeExecutionId, + userId, + actorUserId: billingAttribution.actorUserId, + }) + + /** + * This preflight gives synchronous callers current block/usage feedback + * without reserving under a throwaway id. The claimed resume reruns every + * gate and reserves atomically under its persisted resume execution id. + */ + const preprocessResult = await preprocessExecution({ + workflowId, + userId, + triggerType: 'manual', + executionId: resumeExecutionId, + requestId, + checkRateLimit: false, + checkDeployment: false, + skipConcurrencyReservation: true, + logPreprocessingErrors: false, + workspaceId, + billingAttribution, + }) + + if (!preprocessResult.success) { + logger.warn(`[${requestId}] Preprocessing failed for resume`, { + workflowId, + parentExecutionId: executionId, + error: preprocessResult.error?.message, + statusCode: preprocessResult.error?.statusCode, + }) + + return NextResponse.json( + { + error: + preprocessResult.error?.message || + 'Failed to validate resume execution. Please try again.', + }, + { status: preprocessResult.error?.statusCode || 400 } + ) + } + + logger.info(`[${requestId}] Preprocessing passed, proceeding with resume`, { + workflowId, + parentExecutionId: executionId, + resumeExecutionId, + actorUserId: preprocessResult.actorUserId, + }) + + try { + const enqueueResult = await PauseResumeManager.enqueueOrStartResume({ + executionId, + workflowId, + contextId, + resumeInput, + userId, + allowedPauseKinds: ['human'], + }) + + if (enqueueResult.status === 'queued') { + return NextResponse.json({ + status: 'queued', + executionId: enqueueResult.resumeExecutionId, + queuePosition: enqueueResult.queuePosition, + message: 'Resume queued. It will run after current resumes finish.', + }) + } + + const resumeArgs = { + resumeEntryId: enqueueResult.resumeEntryId, + resumeExecutionId: enqueueResult.resumeExecutionId, + pausedExecution: enqueueResult.pausedExecution, + contextId: enqueueResult.contextId, + resumeInput: enqueueResult.resumeInput, + userId: enqueueResult.userId, + } + + const executionMode = isApiCaller + ? (persistedSnapshot.metadata.executionMode ?? 'sync') + : undefined + const includeThinking = persistedSnapshot.metadata.includeThinking === true + const includeToolCalls = persistedSnapshot.metadata.includeToolCalls === true + + if (isApiCaller && executionMode === 'stream') { + const stream = await createStreamingResponse({ + requestId, + streamConfig: { + selectedOutputs: persistedSnapshot.selectedOutputs, + timeoutMs: preprocessResult.executionTimeout?.sync, + includeThinking, + includeToolCalls, + }, + executionId: enqueueResult.resumeExecutionId, + workspaceId, + workflowId, + userId: enqueueResult.userId, + allowLargeValueWorkflowScope: true, + requestSignal: request.signal, + requestHeaders: request.headers, + executeFn: async ({ onStream, onBlockComplete, abortSignal }) => + PauseResumeManager.startResumeExecution({ + ...resumeArgs, + onStream, + onBlockComplete, + abortSignal, + }), + }) + + return new NextResponse(stream, { + headers: { + ...SSE_HEADERS, + ...agentStreamProtocolResponseHeaders({ requestHeaders: request.headers }), + 'X-Execution-Id': enqueueResult.resumeExecutionId, + }, + }) + } + + if (isApiCaller && executionMode === 'sync') { + const result = await PauseResumeManager.startResumeExecution(resumeArgs) + + return NextResponse.json({ + success: result.success, + status: result.status ?? (result.success ? 'completed' : 'failed'), + executionId: enqueueResult.resumeExecutionId, + output: result.output, + error: result.error, + metadata: result.metadata + ? { + duration: result.metadata.duration, + startTime: result.metadata.startTime, + endTime: result.metadata.endTime, + } + : undefined, + }) + } + + if (isApiCaller && executionMode === 'async') { + const resumePayload: ResumeExecutionPayload = { + resumeEntryId: enqueueResult.resumeEntryId, + resumeExecutionId: enqueueResult.resumeExecutionId, + pausedExecutionId: enqueueResult.pausedExecution.id, + contextId: enqueueResult.contextId, + resumeInput: enqueueResult.resumeInput, + userId: enqueueResult.userId, + workflowId, + parentExecutionId: executionId, + } + + let jobId: string + try { + const jobQueue = await getJobQueue() + jobId = await jobQueue.enqueue('resume-execution', resumePayload, { + ...(pollingSurface === 'v2' + ? { jobId: `${RESUME_EXECUTION_JOB_ID_PREFIX}${enqueueResult.resumeEntryId}` } + : {}), + metadata: { workflowId, workspaceId, userId }, + }) + logger.info('Enqueued async resume execution', { + jobId, + resumeExecutionId: enqueueResult.resumeExecutionId, + }) + } catch (dispatchError) { + logger.error('Failed to dispatch async resume execution', { + error: toError(dispatchError).message, + resumeExecutionId: enqueueResult.resumeExecutionId, + }) + await PauseResumeManager.markResumeAttemptFailed({ + resumeEntryId: enqueueResult.resumeEntryId, + pausedExecutionId: enqueueResult.pausedExecution.id, + parentExecutionId: executionId, + contextId: enqueueResult.contextId, + failureReason: 'Failed to queue async resume execution', + }) + await PauseResumeManager.processQueuedResumes(executionId, workflowId) + return NextResponse.json( + { error: 'Failed to queue resume execution. Please try again.' }, + { status: 503 } + ) + } + + return NextResponse.json( + { + success: true, + async: true, + ...(pollingSurface === 'legacy' ? { jobId } : {}), + executionId: enqueueResult.resumeExecutionId, + message: 'Resume execution queued', + statusUrl: + pollingSurface === 'legacy' + ? `${getBaseUrl()}/api/jobs/${jobId}` + : `${getBaseUrl()}/api/v2/workflows/${workflowId}/executions/${enqueueResult.resumeExecutionId}`, + }, + { status: 202 } + ) + } + + PauseResumeManager.startResumeExecution(resumeArgs).catch((error) => { + logger.error('Failed to start resume execution', { + workflowId, + parentExecutionId: executionId, + resumeExecutionId: enqueueResult.resumeExecutionId, + error, + }) + }) + + return NextResponse.json({ + status: 'started', + executionId: enqueueResult.resumeExecutionId, + message: 'Resume execution started.', + }) + } catch (error) { + logger.error('Resume request failed', { + workflowId, + executionId, + contextId, + error, + }) + const statusCode = + isRecordLike(error) && typeof error.statusCode === 'number' ? error.statusCode : 400 + return NextResponse.json( + { error: toError(error).message || 'Failed to queue resume request' }, + { status: statusCode } + ) + } +} diff --git a/apps/sim/app/api/v2/workflows/[id]/executions/[executionId]/resume/route.test.ts b/apps/sim/app/api/v2/workflows/[id]/executions/[executionId]/resume/route.test.ts new file mode 100644 index 00000000000..5e148d3db93 --- /dev/null +++ b/apps/sim/app/api/v2/workflows/[id]/executions/[executionId]/resume/route.test.ts @@ -0,0 +1,170 @@ +/** + * @vitest-environment node + */ +import { NextRequest, NextResponse } from 'next/server' +import { beforeEach, describe, expect, it, vi } from 'vitest' + +const { mockHandleResumeExecution, mockResolveV2WorkflowAccess } = vi.hoisted(() => ({ + mockHandleResumeExecution: vi.fn(), + mockResolveV2WorkflowAccess: vi.fn(), +})) + +vi.mock('@/app/api/resume/resume-handler', () => ({ + handleResumeExecution: mockHandleResumeExecution, +})) + +vi.mock('@/app/api/v2/workflows/lib/access', () => ({ + resolveV2WorkflowAccess: mockResolveV2WorkflowAccess, +})) + +vi.mock('@/lib/core/utils/urls', () => ({ + getBaseUrl: () => 'https://test.sim.ai', +})) + +import { POST } from '@/app/api/v2/workflows/[id]/executions/[executionId]/resume/route' + +const WORKFLOW_ID = 'workflow-1' +const EXECUTION_ID = 'execution-1' + +function makeRequest(body: string) { + return { + request: new NextRequest( + `http://localhost/api/v2/workflows/${WORKFLOW_ID}/executions/${EXECUTION_ID}/resume`, + { + method: 'POST', + headers: { 'Content-Type': 'application/json', 'X-API-Key': 'test-key' }, + body, + } + ), + context: { params: Promise.resolve({ id: WORKFLOW_ID, executionId: EXECUTION_ID }) }, + } +} + +describe('POST /api/v2/workflows/[id]/executions/[executionId]/resume', () => { + beforeEach(() => { + vi.clearAllMocks() + mockResolveV2WorkflowAccess.mockResolvedValue({ + ok: true, + userId: 'user-1', + keyType: 'workspace', + workflow: { id: WORKFLOW_ID, workspaceId: 'workspace-1' }, + }) + }) + + it('authenticates before parsing the request body', async () => { + mockResolveV2WorkflowAccess.mockResolvedValueOnce({ + ok: false, + response: NextResponse.json( + { error: { code: 'UNAUTHORIZED', message: 'Unauthorized' } }, + { status: 401 } + ), + }) + const { request, context } = makeRequest('{') + + const response = await POST(request, context) + + expect(response.status).toBe(401) + expect(await response.json()).toEqual({ + error: { code: 'UNAUTHORIZED', message: 'Unauthorized' }, + }) + expect(mockResolveV2WorkflowAccess).toHaveBeenCalledWith(request, WORKFLOW_ID, 'write') + expect(mockHandleResumeExecution).not.toHaveBeenCalled() + }) + + it('resumes a pause context through the execution-scoped v2 endpoint', async () => { + mockHandleResumeExecution.mockResolvedValueOnce( + NextResponse.json( + { + success: true, + async: true, + executionId: 'resume-execution-1', + message: 'Resume execution queued', + statusUrl: + 'https://test.sim.ai/api/v2/workflows/workflow-1/executions/resume-execution-1', + }, + { status: 202 } + ) + ) + const { request, context } = makeRequest( + JSON.stringify({ contextId: 'context-1', input: { approved: true } }) + ) + + const response = await POST(request, context) + + expect(response.status).toBe(202) + expect(response.headers.get('X-Execution-Id')).toBe('resume-execution-1') + expect(await response.json()).toEqual({ + data: { + executionId: 'resume-execution-1', + statusUrl: 'https://test.sim.ai/api/v2/workflows/workflow-1/executions/resume-execution-1', + }, + }) + expect(mockHandleResumeExecution).toHaveBeenCalledWith({ + request, + workflowId: WORKFLOW_ID, + executionId: EXECUTION_ID, + contextId: 'context-1', + workspaceId: 'workspace-1', + userId: 'user-1', + resumeInput: { approved: true }, + isApiCaller: true, + pollingSurface: 'v2', + }) + }) + + it('returns queued resumes as a v2 polling receipt', async () => { + mockHandleResumeExecution.mockResolvedValueOnce( + NextResponse.json({ + status: 'queued', + executionId: 'resume-execution-2', + queuePosition: 2, + message: 'Resume queued. It will run after current resumes finish.', + }) + ) + const { request, context } = makeRequest(JSON.stringify({ contextId: 'context-2' })) + + const response = await POST(request, context) + + expect(response.status).toBe(202) + expect(await response.json()).toEqual({ + data: { + executionId: 'resume-execution-2', + statusUrl: 'https://test.sim.ai/api/v2/workflows/workflow-1/executions/resume-execution-2', + queuePosition: 2, + }, + }) + }) + + it('wraps synchronous resume results in the canonical v2 execution shape', async () => { + mockHandleResumeExecution.mockResolvedValueOnce( + NextResponse.json({ + success: true, + status: 'completed', + executionId: 'resume-execution-3', + output: { approved: true }, + metadata: { + startTime: '2026-08-05T00:00:00.000Z', + endTime: '2026-08-05T00:00:01.000Z', + duration: 1000, + }, + }) + ) + const { request, context } = makeRequest(JSON.stringify({ contextId: 'context-3' })) + + const response = await POST(request, context) + + expect(response.status).toBe(200) + expect(await response.json()).toEqual({ + data: { + executionId: 'resume-execution-3', + workflowId: WORKFLOW_ID, + status: 'completed', + output: { approved: true }, + error: null, + startedAt: '2026-08-05T00:00:00.000Z', + endedAt: '2026-08-05T00:00:01.000Z', + durationMs: 1000, + }, + }) + }) +}) diff --git a/apps/sim/app/api/v2/workflows/[id]/executions/[executionId]/resume/route.ts b/apps/sim/app/api/v2/workflows/[id]/executions/[executionId]/resume/route.ts new file mode 100644 index 00000000000..e0afaec0393 --- /dev/null +++ b/apps/sim/app/api/v2/workflows/[id]/executions/[executionId]/resume/route.ts @@ -0,0 +1,147 @@ +import { createLogger } from '@sim/logger' +import { getErrorMessage } from '@sim/utils/errors' +import { isRecordLike } from '@sim/utils/object' +import type { NextRequest } from 'next/server' +import { v2ResumeWorkflowContract } from '@/lib/api/contracts/v2/workflows' +import { WORKFLOW_EXECUTION_ID_HEADER } from '@/lib/api/contracts/workflows' +import { parseRequest } from '@/lib/api/server' +import { getBaseUrl } from '@/lib/core/utils/urls' +import { withRouteHandler } from '@/lib/core/utils/with-route-handler' +import { handleResumeExecution } from '@/app/api/resume/resume-handler' +import { type V2ErrorCode, v2Data, v2Error, v2ValidationError } from '@/app/api/v2/lib/response' +import { resolveV2WorkflowAccess } from '@/app/api/v2/workflows/lib/access' +import { classifyExecutionError } from '@/executor/utils/errors' + +const logger = createLogger('V2WorkflowResumeAPI') + +export const runtime = 'nodejs' +export const dynamic = 'force-dynamic' + +const ERROR_CODE_BY_STATUS: Record = { + 400: 'BAD_REQUEST', + 401: 'UNAUTHORIZED', + 402: 'USAGE_LIMIT_EXCEEDED', + 403: 'FORBIDDEN', + 404: 'NOT_FOUND', + 409: 'CONFLICT', + 413: 'PAYLOAD_TOO_LARGE', + 423: 'LOCKED', + 429: 'RATE_LIMITED', + 503: 'SERVICE_UNAVAILABLE', +} + +const TERMINAL_RESUME_STATUSES = new Set(['completed', 'failed', 'paused', 'cancelled']) + +function errorMessage(payload: Record): string { + return typeof payload.error === 'string' ? payload.error : 'Resume execution failed' +} + +/** + * POST /api/v2/workflows/[id]/executions/[executionId]/resume resumes one pause + * context on the parent execution. The new resume attempt gets its own + * execution ID, which is the only polling handle exposed by v2. + */ +export const POST = withRouteHandler( + async ( + request: NextRequest, + context: { params: Promise<{ id: string; executionId: string }> } + ) => { + const { id: workflowId } = await context.params + const access = await resolveV2WorkflowAccess(request, workflowId, 'write') + if (!access.ok) return access.response + + const parsed = await parseRequest(v2ResumeWorkflowContract, request, context, { + maxBodyBytes: 10 * 1024 * 1024, + validationErrorResponse: v2ValidationError, + }) + if (!parsed.success) return parsed.response + const { executionId } = parsed.data.params + const { contextId, input } = parsed.data.body + + if (!access.workflow.workspaceId) { + return v2Error('INTERNAL_ERROR', 'Workflow has no associated workspace') + } + + try { + const response = await handleResumeExecution({ + request, + workflowId, + executionId, + contextId, + workspaceId: access.workflow.workspaceId, + userId: access.userId, + resumeInput: input === undefined ? {} : input, + isApiCaller: true, + pollingSurface: 'v2', + }) + + if (response.headers.get('Content-Type')?.startsWith('text/event-stream')) { + return response + } + + const payload: unknown = await response.json() + if (!isRecordLike(payload)) { + return v2Error('INTERNAL_ERROR', 'Resume execution returned an invalid response') + } + + if (!response.ok) { + return v2Error( + ERROR_CODE_BY_STATUS[response.status] ?? 'INTERNAL_ERROR', + errorMessage(payload), + { status: response.status } + ) + } + + if (typeof payload.executionId !== 'string') { + return v2Error('INTERNAL_ERROR', 'Resume execution did not return an execution ID') + } + + const statusUrl = `${getBaseUrl()}/api/v2/workflows/${workflowId}/executions/${payload.executionId}` + const headers = { [WORKFLOW_EXECUTION_ID_HEADER]: payload.executionId } + + if (response.status === 202 || payload.status === 'queued') { + return v2Data( + { + executionId: payload.executionId, + statusUrl, + ...(typeof payload.queuePosition === 'number' + ? { queuePosition: payload.queuePosition } + : {}), + }, + { status: 202, headers } + ) + } + + if (typeof payload.status !== 'string' || !TERMINAL_RESUME_STATUSES.has(payload.status)) { + return v2Error('INTERNAL_ERROR', 'Resume execution returned an invalid status') + } + + const metadata = isRecordLike(payload.metadata) ? payload.metadata : undefined + return v2Data( + { + executionId: payload.executionId, + workflowId, + status: payload.status as 'completed' | 'failed' | 'paused' | 'cancelled', + output: payload.output ?? null, + error: + typeof payload.error === 'string' + ? classifyExecutionError(new Error(payload.error)) + : null, + startedAt: + metadata && typeof metadata.startTime === 'string' ? metadata.startTime : undefined, + endedAt: metadata && typeof metadata.endTime === 'string' ? metadata.endTime : undefined, + durationMs: + metadata && typeof metadata.duration === 'number' ? metadata.duration : undefined, + }, + { headers } + ) + } catch (error) { + logger.error('Failed to resume workflow execution', { + workflowId, + executionId, + error: getErrorMessage(error, 'Unknown error'), + }) + return v2Error('INTERNAL_ERROR', 'Internal server error') + } + } +) diff --git a/apps/sim/lib/api/contracts/v2/workflows.ts b/apps/sim/lib/api/contracts/v2/workflows.ts index 10e0faa7c45..8fcae222c9a 100644 --- a/apps/sim/lib/api/contracts/v2/workflows.ts +++ b/apps/sim/lib/api/contracts/v2/workflows.ts @@ -437,6 +437,31 @@ export const v2ExecuteWorkflowContract = defineRouteContract({ }, }) +/** Resume input is scoped to one pause context on the parent execution. */ +export const v2ResumeWorkflowBodySchema = z + .object({ + contextId: z.string().min(1, 'contextId cannot be empty'), + input: z.unknown().optional(), + }) + .strict() +export type V2ResumeWorkflowBody = z.input + +export const v2ResumeWorkflowQueuedSchema = v2ExecuteWorkflowQueuedSchema.extend({ + queuePosition: z.number().int().positive().optional(), +}) +export type V2ResumeWorkflowQueued = z.output + +export const v2ResumeWorkflowContract = defineRouteContract({ + method: 'POST', + path: '/api/v2/workflows/[id]/executions/[executionId]/resume', + params: workflowExecutionParamsSchema, + body: v2ResumeWorkflowBodySchema, + response: { + mode: 'json', + schema: v2DataResponse(v2ExecuteWorkflowDataSchema), + }, +}) + /** * The polled execution resource. `queued` is backfilled from the async job * queue before the worker writes the durable log row — v1's jobs endpoint 404 From c903ba18c5fe0e5a1e29242bfb9d6c906cb8ad0f Mon Sep 17 00:00:00 2001 From: Theodore Li Date: Wed, 5 Aug 2026 15:28:12 -0700 Subject: [PATCH 7/8] fix(api): project pending resume attempts --- .../executor/execution-status.test.ts | 34 +++++++++++++++++-- .../workflows/executor/execution-status.ts | 14 +++++--- 2 files changed, 41 insertions(+), 7 deletions(-) diff --git a/apps/sim/lib/workflows/executor/execution-status.test.ts b/apps/sim/lib/workflows/executor/execution-status.test.ts index 4b45ea3196b..b91a3bb503b 100644 --- a/apps/sim/lib/workflows/executor/execution-status.test.ts +++ b/apps/sim/lib/workflows/executor/execution-status.test.ts @@ -57,7 +57,7 @@ describe('getWorkflowExecutionStatus queue projection', () => { }) it('uses the resume entry ID when the queued work is a resume attempt', async () => { - queueTableRows(schemaMock.resumeQueue, [{ id: 'resume-entry-1' }]) + queueTableRows(schemaMock.resumeQueue, [{ id: 'resume-entry-1', status: 'claimed' }]) mockGetJob.mockResolvedValueOnce({ status: 'processing', createdAt: new Date('2026-08-05T12:00:00.000Z'), @@ -84,7 +84,7 @@ describe('getWorkflowExecutionStatus queue projection', () => { status: 'paused', }, ]) - queueTableRows(schemaMock.resumeQueue, [{ id: 'resume-entry-1' }]) + queueTableRows(schemaMock.resumeQueue, [{ id: 'resume-entry-1', status: 'claimed' }]) mockGetJob.mockResolvedValueOnce({ status: 'pending', createdAt: new Date('2026-08-05T12:00:00.000Z'), @@ -112,6 +112,7 @@ describe('getWorkflowExecutionStatus queue projection', () => { queueTableRows(schemaMock.resumeQueue, [ { id: 'resume-entry-1', + status: 'claimed', queuedAt: new Date('2026-08-05T12:00:00.000Z'), claimedAt: new Date('2026-08-05T12:00:01.000Z'), }, @@ -129,6 +130,35 @@ describe('getWorkflowExecutionStatus queue projection', () => { }) }) + it('projects a pending serialized resume as queued', async () => { + queueTableRows(schemaMock.workflowExecutionLogs, [ + { + executionId: 'execution-1', + workflowId: 'workflow-1', + status: 'paused', + trigger: 'api', + }, + ]) + queueTableRows(schemaMock.resumeQueue, [ + { + id: 'resume-entry-2', + status: 'pending', + queuedAt: new Date('2026-08-05T12:00:02.000Z'), + claimedAt: null, + }, + ]) + + const status = await getWorkflowExecutionStatus(input) + + expect(status).toMatchObject({ + executionId: 'execution-1', + status: 'queued', + startedAt: '2026-08-05T12:00:02.000Z', + paused: null, + }) + expect(mockGetJob).not.toHaveBeenCalled() + }) + it('returns completed queue output when requested', async () => { mockGetJob.mockResolvedValueOnce({ status: 'completed', diff --git a/apps/sim/lib/workflows/executor/execution-status.ts b/apps/sim/lib/workflows/executor/execution-status.ts index 3170f4cf359..2747fa58b94 100644 --- a/apps/sim/lib/workflows/executor/execution-status.ts +++ b/apps/sim/lib/workflows/executor/execution-status.ts @@ -1,6 +1,6 @@ import { db } from '@sim/db' import { pausedExecutions, resumeQueue, workflowExecutionLogs } from '@sim/db/schema' -import { and, eq } from 'drizzle-orm' +import { and, eq, inArray, sql } from 'drizzle-orm' import type { WorkflowExecutionStatusResponse } from '@/lib/api/contracts/workflows' import { getJobQueue } from '@/lib/core/async-jobs' import type { Job } from '@/lib/core/async-jobs/types' @@ -157,6 +157,7 @@ export async function getWorkflowExecutionStatus( const [activeResume] = await db .select({ id: resumeQueue.id, + status: resumeQueue.status, queuedAt: resumeQueue.queuedAt, claimedAt: resumeQueue.claimedAt, }) @@ -165,13 +166,16 @@ export async function getWorkflowExecutionStatus( and( eq(resumeQueue.parentExecutionId, executionId), eq(resumeQueue.newExecutionId, executionId), - eq(resumeQueue.status, 'claimed') + inArray(resumeQueue.status, ['pending', 'claimed'] as const) ) ) + .orderBy(sql`case when ${resumeQueue.status} = 'claimed' then 0 else 1 end`) .limit(1) const queueJobIds = [ - ...(activeResume ? [`${RESUME_EXECUTION_JOB_ID_PREFIX}${activeResume.id}`] : []), + ...(activeResume?.status === 'claimed' + ? [`${RESUME_EXECUTION_JOB_ID_PREFIX}${activeResume.id}`] + : []), ...(!logRow ? [`${WORKFLOW_EXECUTION_JOB_ID_PREFIX}${executionId}`] : []), ] @@ -184,13 +188,13 @@ export async function getWorkflowExecutionStatus( } } - if (activeResume && logRow) { + if (activeResume) { const startedAt = activeResume.claimedAt ?? activeResume.queuedAt return { executionId, workflowId, status: 'queued', - trigger: logRow.trigger, + trigger: logRow?.trigger ?? 'api', level: 'info', startedAt: startedAt.toISOString(), endedAt: null, From 94e28e75df9079def87c3666850089ccf1427d97 Mon Sep 17 00:00:00 2001 From: Theodore Li Date: Wed, 5 Aug 2026 15:34:15 -0700 Subject: [PATCH 8/8] fix(api): prefer terminal logs over stale resumes --- .../executor/execution-status.test.ts | 34 +++++++++++++++++++ .../workflows/executor/execution-status.ts | 12 ++++--- 2 files changed, 42 insertions(+), 4 deletions(-) diff --git a/apps/sim/lib/workflows/executor/execution-status.test.ts b/apps/sim/lib/workflows/executor/execution-status.test.ts index b91a3bb503b..dfbc0719224 100644 --- a/apps/sim/lib/workflows/executor/execution-status.test.ts +++ b/apps/sim/lib/workflows/executor/execution-status.test.ts @@ -159,6 +159,40 @@ describe('getWorkflowExecutionStatus queue projection', () => { expect(mockGetJob).not.toHaveBeenCalled() }) + it('does not let an orphaned pending resume mask a terminal log', async () => { + queueTableRows(schemaMock.workflowExecutionLogs, [ + { + executionId: 'execution-1', + workflowId: 'workflow-1', + workspaceId: 'workspace-1', + status: 'completed', + level: 'info', + trigger: 'api', + startedAt: new Date('2026-08-05T12:00:00.000Z'), + endedAt: new Date('2026-08-05T12:00:01.000Z'), + totalDurationMs: 1000, + executionData: null, + costTotal: null, + }, + ]) + queueTableRows(schemaMock.resumeQueue, [ + { + id: 'resume-entry-2', + status: 'pending', + queuedAt: new Date('2026-08-05T12:00:02.000Z'), + claimedAt: null, + }, + ]) + + const status = await getWorkflowExecutionStatus(input) + + expect(status).toMatchObject({ + executionId: 'execution-1', + status: 'completed', + }) + expect(mockGetJob).not.toHaveBeenCalled() + }) + it('returns completed queue output when requested', async () => { mockGetJob.mockResolvedValueOnce({ status: 'completed', diff --git a/apps/sim/lib/workflows/executor/execution-status.ts b/apps/sim/lib/workflows/executor/execution-status.ts index 2747fa58b94..f90ef18e1c6 100644 --- a/apps/sim/lib/workflows/executor/execution-status.ts +++ b/apps/sim/lib/workflows/executor/execution-status.ts @@ -172,9 +172,13 @@ export async function getWorkflowExecutionStatus( .orderBy(sql`case when ${resumeQueue.status} = 'claimed' then 0 else 1 end`) .limit(1) + const hasTerminalLog = + logRow?.status === 'completed' || logRow?.status === 'failed' || logRow?.status === 'cancelled' + const projectedResume = hasTerminalLog ? undefined : activeResume + const queueJobIds = [ - ...(activeResume?.status === 'claimed' - ? [`${RESUME_EXECUTION_JOB_ID_PREFIX}${activeResume.id}`] + ...(projectedResume?.status === 'claimed' + ? [`${RESUME_EXECUTION_JOB_ID_PREFIX}${projectedResume.id}`] : []), ...(!logRow ? [`${WORKFLOW_EXECUTION_JOB_ID_PREFIX}${executionId}`] : []), ] @@ -188,8 +192,8 @@ export async function getWorkflowExecutionStatus( } } - if (activeResume) { - const startedAt = activeResume.claimedAt ?? activeResume.queuedAt + if (projectedResume) { + const startedAt = projectedResume.claimedAt ?? projectedResume.queuedAt return { executionId, workflowId,