Compare commits

...

33 Commits

Author SHA1 Message Date
Siddharth Ganesan
0d6b485d40 Fix 2026-01-27 20:08:27 -08:00
Siddharth Ganesan
a9f271cdb0 I think it works?? 2026-01-27 20:04:53 -08:00
Siddharth Ganesan
0ead5aa04e Fix 2026-01-27 18:35:33 -08:00
Siddharth Ganesan
28fbd0c086 Fix 2026-01-27 18:25:12 -08:00
Siddharth Ganesan
994a664172 Catch error 2026-01-27 18:22:16 -08:00
Siddharth Ganesan
79857e1a04 Fix bugs 2026-01-27 18:16:46 -08:00
Siddharth Ganesan
07dfedd5f1 Fix test 2026-01-27 18:07:36 -08:00
Siddharth Ganesan
c68cda63ae Cleanup 2026-01-27 17:53:37 -08:00
Siddharth Ganesan
4996eea2ee Fix 2026-01-27 17:38:41 -08:00
Siddharth Ganesan
d9631424dc Fix loops and parallels 2026-01-27 17:08:39 -08:00
Vikhyath Mondreti
08bea875e5 Merge branch 'staging' into feat/run-from-block-2 2026-01-27 17:01:31 -08:00
Siddharth Ganesan
c201a7ca91 Fix trigger clear snapshot 2026-01-27 16:53:13 -08:00
Waleed
bca355c36d feat(tools): added clerk tools and block (#3032)
* feat(tools): added clerk tools and block

* updated docs gen script

* use clerk api types
2026-01-27 16:45:48 -08:00
Siddharth Ganesan
d80608cdd5 Fix 2026-01-27 16:20:41 -08:00
Siddharth Ganesan
6f66d33e62 Fix mock payload 2026-01-27 14:59:34 -08:00
Siddharth Ganesan
2f504ce07e Fix 2026-01-27 14:55:07 -08:00
Vikhyath Mondreti
089427822e fix(hitl): add missing fields to block configs (#3027)
* fix(hitl): add missing fields to block configs

* update copilot paths

* one more case

* update helper usage
2026-01-27 14:35:37 -08:00
Siddharth Ganesan
f55f6cc453 Fix lint 2026-01-27 14:08:32 -08:00
Siddharth Ganesan
c14c614e33 Consolidation 2026-01-27 12:55:27 -08:00
Siddharth Ganesan
415acda403 Allow run from block for triggers 2026-01-27 12:50:16 -08:00
Siddharth Ganesan
8dc45e6e7e Fix 2026-01-27 12:32:18 -08:00
Siddharth Ganesan
7a0aaa460d Clean up 2026-01-27 12:30:46 -08:00
Siddharth Ganesan
2c333bfd98 Lint 2026-01-27 12:25:27 -08:00
Siddharth Ganesan
23ab11a40d Run u ntil block 2026-01-27 12:13:09 -08:00
Siddharth Ganesan
6e541949ec Change ordering 2026-01-27 11:37:36 -08:00
Siddharth Ganesan
3231955a07 Fix loop l ogs 2026-01-27 11:36:09 -08:00
Siddharth Ganesan
d38fb29e05 Fix trace spans 2026-01-27 11:21:42 -08:00
Siddharth Ganesan
5c1e620831 Fix 2026-01-27 11:03:34 -08:00
Siddharth Ganesan
72594df766 Minor improvements 2026-01-27 11:03:13 -08:00
Siddharth Ganesan
be95a7dbd8 Fix 2026-01-27 10:33:31 -08:00
Siddharth Ganesan
da5d4ac9d5 Fix 2026-01-26 17:16:35 -08:00
Siddharth Ganesan
e8534bea7a Fixes 2026-01-26 16:40:14 -08:00
Siddharth Ganesan
3d0b810a8e Run from block 2026-01-26 16:19:41 -08:00
87 changed files with 5648 additions and 1032 deletions

View File

@@ -2096,6 +2096,23 @@ export function ClayIcon(props: SVGProps<SVGSVGElement>) {
)
}
export function ClerkIcon(props: SVGProps<SVGSVGElement>) {
return (
<svg {...props} viewBox='0 0 128 128' fill='none' xmlns='http://www.w3.org/2000/svg'>
<circle cx='64' cy='64' r='20' fill='white' />
<path
d='M99.5716 10.788C101.571 12.1272 101.742 14.9444 100.04 16.646L85.4244 31.2618C84.1035 32.5828 82.0542 32.7914 80.3915 31.9397C75.4752 29.421 69.9035 28 64 28C44.1177 28 28 44.1177 28 64C28 69.9035 29.421 75.4752 31.9397 80.3915C32.7914 82.0542 32.5828 84.1035 31.2618 85.4244L16.646 100.04C14.9444 101.742 12.1272 101.571 10.788 99.5716C3.97411 89.3989 0 77.1635 0 64C0 28.6538 28.6538 0 64 0C77.1635 0 89.3989 3.97411 99.5716 10.788Z'
fill='white'
fillOpacity='0.4'
/>
<path
d='M100.04 111.354C101.742 113.056 101.571 115.873 99.5717 117.212C89.3989 124.026 77.1636 128 64 128C50.8364 128 38.6011 124.026 28.4283 117.212C26.4289 115.873 26.2581 113.056 27.9597 111.354L42.5755 96.7382C43.8965 95.4172 45.9457 95.2085 47.6084 96.0603C52.5248 98.579 58.0964 100 64 100C69.9036 100 75.4753 98.579 80.3916 96.0603C82.0543 95.2085 84.1036 95.4172 85.4245 96.7382L100.04 111.354Z'
fill='white'
/>
</svg>
)
}
export function MicrosoftIcon(props: SVGProps<SVGSVGElement>) {
return (
<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 23 23' {...props}>

View File

@@ -16,6 +16,7 @@ import {
CalendlyIcon,
CirclebackIcon,
ClayIcon,
ClerkIcon,
ConfluenceIcon,
CursorIcon,
DatadogIcon,
@@ -143,6 +144,7 @@ export const blockTypeToIconMap: Record<string, IconComponent> = {
calendly: CalendlyIcon,
circleback: CirclebackIcon,
clay: ClayIcon,
clerk: ClerkIcon,
confluence_v2: ConfluenceIcon,
cursor_v2: CursorIcon,
datadog: DatadogIcon,

View File

@@ -168,10 +168,10 @@ Search for tasks in an Asana workspace
| `success` | boolean | Operation success status |
| `ts` | string | Timestamp of the response |
| `tasks` | array | Array of matching tasks |
| ↳ `gid` | string | Assignee GID |
| ↳ `gid` | string | Task GID |
| ↳ `resource_type` | string | Resource type |
| ↳ `resource_subtype` | string | Resource subtype |
| ↳ `name` | string | Assignee name |
| ↳ `name` | string | Task name |
| ↳ `notes` | string | Task notes |
| ↳ `completed` | boolean | Completion status |
| ↳ `assignee` | object | Assignee details |

View File

@@ -116,7 +116,7 @@ Get detailed information about a specific event type
| --------- | ---- | ----------- |
| `resource` | object | Event type details |
| ↳ `uri` | string | Canonical reference to the event type |
| ↳ `name` | string | Question text |
| ↳ `name` | string | Event type name |
| ↳ `active` | boolean | Whether the event type is active |
| ↳ `booking_method` | string | Booking method |
| ↳ `color` | string | Hex color code |
@@ -128,16 +128,12 @@ Get detailed information about a specific event type
| ↳ `enabled` | boolean | Whether question is enabled |
| ↳ `required` | boolean | Whether question is required |
| ↳ `answer_choices` | array | Available answer choices |
| ↳ `type` | string | Event type classification |
| ↳ `position` | number | Question order |
| ↳ `enabled` | boolean | Whether question is enabled |
| ↳ `required` | boolean | Whether question is required |
| ↳ `answer_choices` | array | Available answer choices |
| ↳ `description_html` | string | HTML formatted description |
| ↳ `description_plain` | string | Plain text description |
| ↳ `duration` | number | Duration in minutes |
| ↳ `scheduling_url` | string | URL to scheduling page |
| ↳ `slug` | string | Unique identifier for URLs |
| ↳ `type` | string | Event type classification |
| ↳ `updated_at` | string | ISO timestamp of last update |
### `calendly_list_scheduled_events`
@@ -170,16 +166,14 @@ Retrieve a list of scheduled events for a user or organization
| ↳ `start_time` | string | ISO timestamp of event start |
| ↳ `end_time` | string | ISO timestamp of event end |
| ↳ `event_type` | string | URI of the event type |
| ↳ `location` | string | Location description |
| ↳ `location` | object | Event location details |
| ↳ `type` | string | Location type \(e.g., |
| ↳ `location` | string | Location description |
| ↳ `join_url` | string | URL to join online meeting \(if applicable\) |
| ↳ `invitees_counter` | object | Invitee count information |
| ↳ `total` | number | Total number of invitees |
| ↳ `active` | number | Number of active invitees |
| ↳ `limit` | number | Maximum number of invitees |
| ↳ `total` | number | Total number of invitees |
| ↳ `active` | number | Number of active invitees |
| ↳ `limit` | number | Maximum number of invitees |
| ↳ `created_at` | string | ISO timestamp of event creation |
| ↳ `updated_at` | string | ISO timestamp of last update |
| `pagination` | object | Pagination information |
@@ -211,28 +205,22 @@ Get detailed information about a specific scheduled event
| ↳ `start_time` | string | ISO timestamp of event start |
| ↳ `end_time` | string | ISO timestamp of event end |
| ↳ `event_type` | string | URI of the event type |
| ↳ `location` | string | Location description |
| ↳ `type` | string | Location type |
| ↳ `join_url` | string | URL to join online meeting |
| ↳ `location` | object | Event location details |
| ↳ `type` | string | Location type |
| ↳ `location` | string | Location description |
| ↳ `join_url` | string | URL to join online meeting |
| ↳ `invitees_counter` | object | Invitee count information |
| ↳ `total` | number | Total number of invitees |
| ↳ `active` | number | Number of active invitees |
| ↳ `limit` | number | Maximum number of invitees |
| ↳ `total` | number | Total number of invitees |
| ↳ `active` | number | Number of active invitees |
| ↳ `limit` | number | Maximum number of invitees |
| ↳ `event_memberships` | array | Event hosts/members |
| ↳ `user` | string | User URI |
| ↳ `user_email` | string | User email |
| ↳ `user_name` | string | User name |
| ↳ `user` | string | User URI |
| ↳ `user_email` | string | User email |
| ↳ `user_name` | string | User name |
| ↳ `event_guests` | array | Additional guests |
| ↳ `email` | string | Guest email |
| ↳ `created_at` | string | When guest was added |
| ↳ `updated_at` | string | When guest info was updated |
| ↳ `email` | string | Guest email |
| ↳ `created_at` | string | ISO timestamp of event creation |
| ↳ `updated_at` | string | ISO timestamp of last update |
@@ -267,9 +255,6 @@ Retrieve a list of invitees for a scheduled event
| ↳ `question` | string | Question text |
| ↳ `answer` | string | Invitee answer |
| ↳ `position` | number | Question order |
| ↳ `question` | string | Question text |
| ↳ `answer` | string | Invitee answer |
| ↳ `position` | number | Question order |
| ↳ `timezone` | string | Invitee timezone |
| ↳ `event` | string | URI of the scheduled event |
| ↳ `created_at` | string | ISO timestamp when invitee was created |

View File

@@ -0,0 +1,442 @@
---
title: Clerk
description: Manage users, organizations, and sessions in Clerk
---
import { BlockInfoCard } from "@/components/ui/block-info-card"
<BlockInfoCard
type="clerk"
color="#131316"
/>
{/* MANUAL-CONTENT-START:intro */}
[Clerk](https://clerk.com/) is a comprehensive identity infrastructure platform that helps you manage users, authentication, and sessions for your applications.
In Sim, the Clerk integration lets your agents automate user and session management through easy-to-use API-based tools. Agents can securely list users, update user profiles, manage organizations, monitor sessions, and revoke access directly in your workflow.
With Clerk, you can:
- **Authenticate users and manage sessions**: Seamlessly control sign-in, sign-up, and session lifecycle for your users.
- **List and update users**: Automatically pull user lists, update user attributes, or view profile details as part of your agent tasks.
- **Manage organizations and memberships**: Add or update organizations and administer user memberships with clarity.
- **Monitor and revoke sessions**: See active or past user sessions, and revoke access immediately if needed for security.
The integration enables real-time, auditable management of your user base—all from within Sim. Connected agents can automate onboarding, enforce policies, keep directories up to date, and react to authentication events or organizational changes, helping you run secure and flexible processes using Clerk as your identity engine.
{/* MANUAL-CONTENT-END */}
## Usage Instructions
Integrate Clerk authentication and user management into your workflow. Create, update, delete, and list users. Manage organizations and their memberships. Monitor and control user sessions.
## Tools
### `clerk_list_users`
List all users in your Clerk application with optional filtering and pagination
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `secretKey` | string | Yes | The Clerk Secret Key for API authentication |
| `limit` | number | No | Number of results per page \(1-500, default: 10\) |
| `offset` | number | No | Number of results to skip for pagination |
| `orderBy` | string | No | Sort field with optional +/- prefix for direction \(default: -created_at\) |
| `emailAddress` | string | No | Filter by email address \(comma-separated for multiple\) |
| `phoneNumber` | string | No | Filter by phone number \(comma-separated for multiple\) |
| `externalId` | string | No | Filter by external ID \(comma-separated for multiple\) |
| `username` | string | No | Filter by username \(comma-separated for multiple\) |
| `userId` | string | No | Filter by user ID \(comma-separated for multiple\) |
| `query` | string | No | Search query to match across email, phone, username, and names |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `users` | array | Array of Clerk user objects |
| ↳ `id` | string | User ID |
| ↳ `username` | string | Username |
| ↳ `firstName` | string | First name |
| ↳ `lastName` | string | Last name |
| ↳ `imageUrl` | string | Profile image URL |
| ↳ `hasImage` | boolean | Whether user has a profile image |
| ↳ `primaryEmailAddressId` | string | Primary email address ID |
| ↳ `primaryPhoneNumberId` | string | Primary phone number ID |
| ↳ `emailAddresses` | array | User email addresses |
| ↳ `id` | string | Email address ID |
| ↳ `emailAddress` | string | Email address |
| ↳ `phoneNumbers` | array | User phone numbers |
| ↳ `id` | string | Phone number ID |
| ↳ `phoneNumber` | string | Phone number |
| ↳ `externalId` | string | External system ID |
| ↳ `passwordEnabled` | boolean | Whether password is enabled |
| ↳ `twoFactorEnabled` | boolean | Whether 2FA is enabled |
| ↳ `banned` | boolean | Whether user is banned |
| ↳ `locked` | boolean | Whether user is locked |
| ↳ `lastSignInAt` | number | Last sign-in timestamp |
| ↳ `lastActiveAt` | number | Last activity timestamp |
| ↳ `createdAt` | number | Creation timestamp |
| ↳ `updatedAt` | number | Last update timestamp |
| ↳ `publicMetadata` | json | Public metadata |
| `totalCount` | number | Total number of users matching the query |
| `success` | boolean | Operation success status |
### `clerk_get_user`
Retrieve a single user by their ID from Clerk
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `secretKey` | string | Yes | The Clerk Secret Key for API authentication |
| `userId` | string | Yes | The ID of the user to retrieve |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `id` | string | User ID |
| `username` | string | Username |
| `firstName` | string | First name |
| `lastName` | string | Last name |
| `imageUrl` | string | Profile image URL |
| `hasImage` | boolean | Whether user has a profile image |
| `primaryEmailAddressId` | string | Primary email address ID |
| `primaryPhoneNumberId` | string | Primary phone number ID |
| `primaryWeb3WalletId` | string | Primary Web3 wallet ID |
| `emailAddresses` | array | User email addresses |
| ↳ `id` | string | Email address ID |
| ↳ `emailAddress` | string | Email address |
| ↳ `verified` | boolean | Whether email is verified |
| `phoneNumbers` | array | User phone numbers |
| ↳ `id` | string | Phone number ID |
| ↳ `phoneNumber` | string | Phone number |
| ↳ `verified` | boolean | Whether phone is verified |
| `externalId` | string | External system ID |
| `passwordEnabled` | boolean | Whether password is enabled |
| `twoFactorEnabled` | boolean | Whether 2FA is enabled |
| `totpEnabled` | boolean | Whether TOTP is enabled |
| `backupCodeEnabled` | boolean | Whether backup codes are enabled |
| `banned` | boolean | Whether user is banned |
| `locked` | boolean | Whether user is locked |
| `deleteSelfEnabled` | boolean | Whether user can delete themselves |
| `createOrganizationEnabled` | boolean | Whether user can create organizations |
| `lastSignInAt` | number | Last sign-in timestamp |
| `lastActiveAt` | number | Last activity timestamp |
| `createdAt` | number | Creation timestamp |
| `updatedAt` | number | Last update timestamp |
| `publicMetadata` | json | Public metadata \(readable from frontend\) |
| `privateMetadata` | json | Private metadata \(backend only\) |
| `unsafeMetadata` | json | Unsafe metadata \(modifiable from frontend\) |
| `success` | boolean | Operation success status |
### `clerk_create_user`
Create a new user in your Clerk application
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `secretKey` | string | Yes | The Clerk Secret Key for API authentication |
| `emailAddress` | string | No | Email addresses for the user \(comma-separated for multiple\) |
| `phoneNumber` | string | No | Phone numbers for the user \(comma-separated for multiple\) |
| `username` | string | No | Username for the user \(must be unique\) |
| `password` | string | No | Password for the user \(minimum 8 characters\) |
| `firstName` | string | No | First name of the user |
| `lastName` | string | No | Last name of the user |
| `externalId` | string | No | External system identifier \(must be unique\) |
| `publicMetadata` | json | No | Public metadata \(JSON object, readable from frontend\) |
| `privateMetadata` | json | No | Private metadata \(JSON object, backend only\) |
| `unsafeMetadata` | json | No | Unsafe metadata \(JSON object, modifiable from frontend\) |
| `skipPasswordChecks` | boolean | No | Skip password validation checks |
| `skipPasswordRequirement` | boolean | No | Make password optional |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `id` | string | Created user ID |
| `username` | string | Username |
| `firstName` | string | First name |
| `lastName` | string | Last name |
| `imageUrl` | string | Profile image URL |
| `primaryEmailAddressId` | string | Primary email address ID |
| `primaryPhoneNumberId` | string | Primary phone number ID |
| `emailAddresses` | array | User email addresses |
| ↳ `id` | string | Email address ID |
| ↳ `emailAddress` | string | Email address |
| ↳ `verified` | boolean | Whether email is verified |
| `phoneNumbers` | array | User phone numbers |
| ↳ `id` | string | Phone number ID |
| ↳ `phoneNumber` | string | Phone number |
| ↳ `verified` | boolean | Whether phone is verified |
| `externalId` | string | External system ID |
| `createdAt` | number | Creation timestamp |
| `updatedAt` | number | Last update timestamp |
| `publicMetadata` | json | Public metadata |
| `success` | boolean | Operation success status |
### `clerk_update_user`
Update an existing user in your Clerk application
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `secretKey` | string | Yes | The Clerk Secret Key for API authentication |
| `userId` | string | Yes | The ID of the user to update |
| `firstName` | string | No | First name of the user |
| `lastName` | string | No | Last name of the user |
| `username` | string | No | Username \(must be unique\) |
| `password` | string | No | New password \(minimum 8 characters\) |
| `externalId` | string | No | External system identifier |
| `primaryEmailAddressId` | string | No | ID of verified email to set as primary |
| `primaryPhoneNumberId` | string | No | ID of verified phone to set as primary |
| `publicMetadata` | json | No | Public metadata \(JSON object\) |
| `privateMetadata` | json | No | Private metadata \(JSON object\) |
| `unsafeMetadata` | json | No | Unsafe metadata \(JSON object\) |
| `skipPasswordChecks` | boolean | No | Skip password validation checks |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `id` | string | Updated user ID |
| `username` | string | Username |
| `firstName` | string | First name |
| `lastName` | string | Last name |
| `imageUrl` | string | Profile image URL |
| `primaryEmailAddressId` | string | Primary email address ID |
| `primaryPhoneNumberId` | string | Primary phone number ID |
| `emailAddresses` | array | User email addresses |
| ↳ `id` | string | Email address ID |
| ↳ `emailAddress` | string | Email address |
| ↳ `verified` | boolean | Whether email is verified |
| `phoneNumbers` | array | User phone numbers |
| ↳ `id` | string | Phone number ID |
| ↳ `phoneNumber` | string | Phone number |
| ↳ `verified` | boolean | Whether phone is verified |
| `externalId` | string | External system ID |
| `banned` | boolean | Whether user is banned |
| `locked` | boolean | Whether user is locked |
| `createdAt` | number | Creation timestamp |
| `updatedAt` | number | Last update timestamp |
| `publicMetadata` | json | Public metadata |
| `success` | boolean | Operation success status |
### `clerk_delete_user`
Delete a user from your Clerk application
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `secretKey` | string | Yes | The Clerk Secret Key for API authentication |
| `userId` | string | Yes | The ID of the user to delete |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `id` | string | Deleted user ID |
| `object` | string | Object type \(user\) |
| `deleted` | boolean | Whether the user was deleted |
| `success` | boolean | Operation success status |
### `clerk_list_organizations`
List all organizations in your Clerk application with optional filtering
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `secretKey` | string | Yes | The Clerk Secret Key for API authentication |
| `limit` | number | No | Number of results per page \(1-500, default: 10\) |
| `offset` | number | No | Number of results to skip for pagination |
| `includeMembersCount` | boolean | No | Include member count for each organization |
| `query` | string | No | Search by organization ID, name, or slug |
| `orderBy` | string | No | Sort field \(name, created_at, members_count\) with +/- prefix |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `organizations` | array | Array of Clerk organization objects |
| ↳ `id` | string | Organization ID |
| ↳ `name` | string | Organization name |
| ↳ `slug` | string | Organization slug |
| ↳ `imageUrl` | string | Organization image URL |
| ↳ `hasImage` | boolean | Whether organization has an image |
| ↳ `membersCount` | number | Number of members |
| ↳ `pendingInvitationsCount` | number | Number of pending invitations |
| ↳ `maxAllowedMemberships` | number | Max allowed memberships |
| ↳ `adminDeleteEnabled` | boolean | Whether admin delete is enabled |
| ↳ `createdBy` | string | Creator user ID |
| ↳ `createdAt` | number | Creation timestamp |
| ↳ `updatedAt` | number | Last update timestamp |
| ↳ `publicMetadata` | json | Public metadata |
| `totalCount` | number | Total number of organizations |
| `success` | boolean | Operation success status |
### `clerk_get_organization`
Retrieve a single organization by ID or slug from Clerk
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `secretKey` | string | Yes | The Clerk Secret Key for API authentication |
| `organizationId` | string | Yes | The ID or slug of the organization to retrieve |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `id` | string | Organization ID |
| `name` | string | Organization name |
| `slug` | string | Organization slug |
| `imageUrl` | string | Organization image URL |
| `hasImage` | boolean | Whether organization has an image |
| `membersCount` | number | Number of members |
| `pendingInvitationsCount` | number | Number of pending invitations |
| `maxAllowedMemberships` | number | Max allowed memberships |
| `adminDeleteEnabled` | boolean | Whether admin delete is enabled |
| `createdBy` | string | Creator user ID |
| `createdAt` | number | Creation timestamp |
| `updatedAt` | number | Last update timestamp |
| `publicMetadata` | json | Public metadata |
| `success` | boolean | Operation success status |
### `clerk_create_organization`
Create a new organization in your Clerk application
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `secretKey` | string | Yes | The Clerk Secret Key for API authentication |
| `name` | string | Yes | Name of the organization |
| `createdBy` | string | Yes | User ID of the creator \(will become admin\) |
| `slug` | string | No | Slug identifier for the organization |
| `maxAllowedMemberships` | number | No | Maximum member capacity \(0 for unlimited\) |
| `publicMetadata` | json | No | Public metadata \(JSON object\) |
| `privateMetadata` | json | No | Private metadata \(JSON object\) |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `id` | string | Created organization ID |
| `name` | string | Organization name |
| `slug` | string | Organization slug |
| `imageUrl` | string | Organization image URL |
| `hasImage` | boolean | Whether organization has an image |
| `membersCount` | number | Number of members |
| `pendingInvitationsCount` | number | Number of pending invitations |
| `maxAllowedMemberships` | number | Max allowed memberships |
| `adminDeleteEnabled` | boolean | Whether admin delete is enabled |
| `createdBy` | string | Creator user ID |
| `createdAt` | number | Creation timestamp |
| `updatedAt` | number | Last update timestamp |
| `publicMetadata` | json | Public metadata |
| `success` | boolean | Operation success status |
### `clerk_list_sessions`
List sessions for a user or client in your Clerk application
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `secretKey` | string | Yes | The Clerk Secret Key for API authentication |
| `userId` | string | No | User ID to list sessions for \(required if clientId not provided\) |
| `clientId` | string | No | Client ID to list sessions for \(required if userId not provided\) |
| `status` | string | No | Filter by session status \(abandoned, active, ended, expired, pending, removed, replaced, revoked\) |
| `limit` | number | No | Number of results per page \(1-500, default: 10\) |
| `offset` | number | No | Number of results to skip for pagination |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `sessions` | array | Array of Clerk session objects |
| ↳ `id` | string | Session ID |
| ↳ `userId` | string | User ID |
| ↳ `clientId` | string | Client ID |
| ↳ `status` | string | Session status |
| ↳ `lastActiveAt` | number | Last activity timestamp |
| ↳ `lastActiveOrganizationId` | string | Last active organization ID |
| ↳ `expireAt` | number | Expiration timestamp |
| ↳ `abandonAt` | number | Abandon timestamp |
| ↳ `createdAt` | number | Creation timestamp |
| ↳ `updatedAt` | number | Last update timestamp |
| `totalCount` | number | Total number of sessions |
| `success` | boolean | Operation success status |
### `clerk_get_session`
Retrieve a single session by ID from Clerk
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `secretKey` | string | Yes | The Clerk Secret Key for API authentication |
| `sessionId` | string | Yes | The ID of the session to retrieve |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `id` | string | Session ID |
| `userId` | string | User ID |
| `clientId` | string | Client ID |
| `status` | string | Session status |
| `lastActiveAt` | number | Last activity timestamp |
| `lastActiveOrganizationId` | string | Last active organization ID |
| `expireAt` | number | Expiration timestamp |
| `abandonAt` | number | Abandon timestamp |
| `createdAt` | number | Creation timestamp |
| `updatedAt` | number | Last update timestamp |
| `success` | boolean | Operation success status |
### `clerk_revoke_session`
Revoke a session to immediately invalidate it
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `secretKey` | string | Yes | The Clerk Secret Key for API authentication |
| `sessionId` | string | Yes | The ID of the session to revoke |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `id` | string | Session ID |
| `userId` | string | User ID |
| `clientId` | string | Client ID |
| `status` | string | Session status \(should be revoked\) |
| `lastActiveAt` | number | Last activity timestamp |
| `lastActiveOrganizationId` | string | Last active organization ID |
| `expireAt` | number | Expiration timestamp |
| `abandonAt` | number | Abandon timestamp |
| `createdAt` | number | Creation timestamp |
| `updatedAt` | number | Last update timestamp |
| `success` | boolean | Operation success status |

View File

@@ -257,11 +257,6 @@ Search and retrieve logs from Datadog. Use for troubleshooting, analysis, or mon
| ↳ `service` | string | Service name |
| ↳ `message` | string | Log message |
| ↳ `status` | string | Log status/level |
| ↳ `timestamp` | string | Log timestamp |
| ↳ `host` | string | Host name |
| ↳ `service` | string | Service name |
| ↳ `message` | string | Log message |
| ↳ `status` | string | Log status/level |
| `nextLogId` | string | Cursor for pagination |
### `datadog_send_logs`

View File

@@ -64,7 +64,7 @@ Send a message to a Discord channel
| --------- | ---- | ----------- |
| `message` | string | Success or error message |
| `data` | object | Discord message data |
| ↳ `id` | string | Author user ID |
| ↳ `id` | string | Message ID |
| ↳ `content` | string | Message content |
| ↳ `channel_id` | string | Channel ID where message was sent |
| ↳ `author` | object | Message author information |
@@ -72,9 +72,6 @@ Send a message to a Discord channel
| ↳ `username` | string | Author username |
| ↳ `avatar` | string | Author avatar hash |
| ↳ `bot` | boolean | Whether author is a bot |
| ↳ `username` | string | Author username |
| ↳ `avatar` | string | Author avatar hash |
| ↳ `bot` | boolean | Whether author is a bot |
| ↳ `timestamp` | string | Message timestamp |
| ↳ `edited_timestamp` | string | Message edited timestamp |
| ↳ `embeds` | array | Message embeds |
@@ -102,7 +99,7 @@ Retrieve messages from a Discord channel
| `message` | string | Success or error message |
| `data` | object | Container for messages data |
| ↳ `messages` | array | Array of Discord messages with full metadata |
| ↳ `id` | string | Author user ID |
| ↳ `id` | string | Message ID |
| ↳ `content` | string | Message content |
| ↳ `channel_id` | string | Channel ID |
| ↳ `author` | object | Message author information |
@@ -110,9 +107,6 @@ Retrieve messages from a Discord channel
| ↳ `username` | string | Author username |
| ↳ `avatar` | string | Author avatar hash |
| ↳ `bot` | boolean | Whether author is a bot |
| ↳ `username` | string | Author username |
| ↳ `avatar` | string | Author avatar hash |
| ↳ `bot` | boolean | Whether author is a bot |
| ↳ `timestamp` | string | Message timestamp |
| ↳ `edited_timestamp` | string | Message edited timestamp |
| ↳ `embeds` | array | Message embeds |
@@ -120,24 +114,7 @@ Retrieve messages from a Discord channel
| ↳ `mentions` | array | User mentions in message |
| ↳ `mention_roles` | array | Role mentions in message |
| ↳ `mention_everyone` | boolean | Whether message mentions everyone |
| ↳ `id` | string | Author user ID |
| ↳ `content` | string | Message content |
| ↳ `channel_id` | string | Channel ID |
| ↳ `author` | object | Message author information |
| ↳ `id` | string | Author user ID |
| ↳ `username` | string | Author username |
| ↳ `avatar` | string | Author avatar hash |
| ↳ `bot` | boolean | Whether author is a bot |
| ↳ `username` | string | Author username |
| ↳ `avatar` | string | Author avatar hash |
| ↳ `bot` | boolean | Whether author is a bot |
| ↳ `timestamp` | string | Message timestamp |
| ↳ `edited_timestamp` | string | Message edited timestamp |
| ↳ `embeds` | array | Message embeds |
| ↳ `attachments` | array | Message attachments |
| ↳ `mentions` | array | User mentions in message |
| ↳ `mention_roles` | array | Role mentions in message |
| ↳ `mention_everyone` | boolean | Whether message mentions everyone |
### `discord_get_server`
@@ -681,9 +658,6 @@ Get information about a member in a Discord server
| ↳ `id` | string | User ID |
| ↳ `username` | string | Username |
| ↳ `avatar` | string | Avatar hash |
| ↳ `id` | string | User ID |
| ↳ `username` | string | Username |
| ↳ `avatar` | string | Avatar hash |
| ↳ `nick` | string | Server nickname |
| ↳ `roles` | array | Array of role IDs |
| ↳ `joined_at` | string | When the member joined |

View File

@@ -105,11 +105,6 @@ Crawl entire websites and extract structured content from all accessible pages
| ↳ `language` | string | Page language |
| ↳ `sourceURL` | string | Source URL of the page |
| ↳ `statusCode` | number | HTTP status code |
| ↳ `title` | string | Page title |
| ↳ `description` | string | Page description |
| ↳ `language` | string | Page language |
| ↳ `sourceURL` | string | Source URL of the page |
| ↳ `statusCode` | number | HTTP status code |
| `total` | number | Total number of pages found during crawl |
| `creditsUsed` | number | Number of credits consumed by the crawl operation |

View File

@@ -1557,20 +1557,20 @@ Search for code across GitHub repositories. Use qualifiers like repo:owner/name,
| `total_count` | number | Total matching results |
| `incomplete_results` | boolean | Whether results are incomplete |
| `items` | array | Array of code matches from GitHub API |
| ↳ `name` | string | Repository name |
| ↳ `name` | string | File name |
| ↳ `path` | string | File path |
| ↳ `sha` | string | Blob SHA |
| ↳ `url` | string | API URL |
| ↳ `git_url` | string | Git blob URL |
| ↳ `html_url` | string | Profile page URL |
| ↳ `html_url` | string | GitHub web URL |
| ↳ `score` | number | Search relevance score |
| ↳ `repository` | object | Repository containing the code |
| ↳ `id` | number | User ID |
| ↳ `id` | number | Repository ID |
| ↳ `node_id` | string | GraphQL node ID |
| ↳ `name` | string | Repository name |
| ↳ `full_name` | string | Full name \(owner/repo\) |
| ↳ `private` | boolean | Whether repository is private |
| ↳ `html_url` | string | Profile page URL |
| ↳ `html_url` | string | GitHub web URL |
| ↳ `description` | string | Repository description |
| ↳ `fork` | boolean | Whether this is a fork |
| ↳ `url` | string | API URL |
@@ -1583,29 +1583,6 @@ Search for code across GitHub repositories. Use qualifiers like repo:owner/name,
| ↳ `html_url` | string | Profile page URL |
| ↳ `type` | string | User or Organization |
| ↳ `site_admin` | boolean | GitHub staff indicator |
| ↳ `login` | string | Username |
| ↳ `avatar_url` | string | Avatar image URL |
| ↳ `type` | string | User or Organization |
| ↳ `site_admin` | boolean | GitHub staff indicator |
| ↳ `id` | number | User ID |
| ↳ `node_id` | string | GraphQL node ID |
| ↳ `full_name` | string | Full name \(owner/repo\) |
| ↳ `private` | boolean | Whether repository is private |
| ↳ `description` | string | Repository description |
| ↳ `fork` | boolean | Whether this is a fork |
| ↳ `owner` | object | Repository owner |
| ↳ `login` | string | Username |
| ↳ `id` | number | User ID |
| ↳ `node_id` | string | GraphQL node ID |
| ↳ `avatar_url` | string | Avatar image URL |
| ↳ `url` | string | API URL |
| ↳ `html_url` | string | Profile page URL |
| ↳ `type` | string | User or Organization |
| ↳ `site_admin` | boolean | GitHub staff indicator |
| ↳ `login` | string | Username |
| ↳ `avatar_url` | string | Avatar image URL |
| ↳ `type` | string | User or Organization |
| ↳ `site_admin` | boolean | GitHub staff indicator |
| ↳ `text_matches` | array | Text matches showing context |
| ↳ `object_url` | string | Object URL |
| ↳ `object_type` | string | Object type |
@@ -1614,17 +1591,6 @@ Search for code across GitHub repositories. Use qualifiers like repo:owner/name,
| ↳ `matches` | array | Match indices |
| ↳ `text` | string | Matched text |
| ↳ `indices` | array | Start and end indices |
| ↳ `text` | string | Matched text |
| ↳ `indices` | array | Start and end indices |
| ↳ `object_url` | string | Object URL |
| ↳ `object_type` | string | Object type |
| ↳ `property` | string | Property matched |
| ↳ `fragment` | string | Text fragment with match |
| ↳ `matches` | array | Match indices |
| ↳ `text` | string | Matched text |
| ↳ `indices` | array | Start and end indices |
| ↳ `text` | string | Matched text |
| ↳ `indices` | array | Start and end indices |
### `github_search_commits`
@@ -1648,23 +1614,20 @@ Search for commits across GitHub. Use qualifiers like repo:owner/name, author:us
| `total_count` | number | Total matching results |
| `incomplete_results` | boolean | Whether results are incomplete |
| `items` | array | Array of commit objects from GitHub API |
| ↳ `sha` | string | Parent SHA |
| ↳ `sha` | string | Commit SHA |
| ↳ `node_id` | string | GraphQL node ID |
| ↳ `html_url` | string | Parent web URL |
| ↳ `url` | string | Parent API URL |
| ↳ `html_url` | string | Web URL |
| ↳ `url` | string | API URL |
| ↳ `comments_url` | string | Comments API URL |
| ↳ `score` | number | Search relevance score |
| ↳ `commit` | object | Core commit data |
| ↳ `url` | string | Tree API URL |
| ↳ `url` | string | Commit API URL |
| ↳ `message` | string | Commit message |
| ↳ `comment_count` | number | Number of comments |
| ↳ `author` | object | Git author |
| ↳ `name` | string | Author name |
| ↳ `email` | string | Author email |
| ↳ `date` | string | Author date \(ISO 8601\) |
| ↳ `name` | string | Committer name |
| ↳ `email` | string | Committer email |
| ↳ `date` | string | Commit date \(ISO 8601\) |
| ↳ `committer` | object | Git committer |
| ↳ `name` | string | Committer name |
| ↳ `email` | string | Committer email |
@@ -1672,9 +1635,6 @@ Search for commits across GitHub. Use qualifiers like repo:owner/name, author:us
| ↳ `tree` | object | Tree object |
| ↳ `sha` | string | Tree SHA |
| ↳ `url` | string | Tree API URL |
| ↳ `sha` | string | Tree SHA |
| ↳ `message` | string | Commit message |
| ↳ `comment_count` | number | Number of comments |
| ↳ `author` | object | GitHub user \(author\) |
| ↳ `login` | string | Username |
| ↳ `id` | number | User ID |
@@ -1684,9 +1644,6 @@ Search for commits across GitHub. Use qualifiers like repo:owner/name, author:us
| ↳ `html_url` | string | Profile URL |
| ↳ `type` | string | User or Organization |
| ↳ `site_admin` | boolean | GitHub staff indicator |
| ↳ `name` | string | Repository name |
| ↳ `email` | string | Committer email |
| ↳ `date` | string | Commit date \(ISO 8601\) |
| ↳ `committer` | object | GitHub user \(committer\) |
| ↳ `login` | string | Username |
| ↳ `id` | number | User ID |
@@ -1696,38 +1653,13 @@ Search for commits across GitHub. Use qualifiers like repo:owner/name, author:us
| ↳ `html_url` | string | Profile URL |
| ↳ `type` | string | User or Organization |
| ↳ `site_admin` | boolean | GitHub staff indicator |
| ↳ `tree` | object | Tree object |
| ↳ `sha` | string | Tree SHA |
| ↳ `url` | string | Tree API URL |
| ↳ `login` | string | Username |
| ↳ `id` | number | User ID |
| ↳ `avatar_url` | string | Avatar image URL |
| ↳ `type` | string | User or Organization |
| ↳ `site_admin` | boolean | GitHub staff indicator |
| ↳ `repository` | object | Repository containing the commit |
| ↳ `id` | number | User ID |
| ↳ `id` | number | Repository ID |
| ↳ `node_id` | string | GraphQL node ID |
| ↳ `name` | string | Repository name |
| ↳ `full_name` | string | Full name \(owner/repo\) |
| ↳ `private` | boolean | Whether repository is private |
| ↳ `html_url` | string | Profile page URL |
| ↳ `description` | string | Repository description |
| ↳ `owner` | object | Repository owner |
| ↳ `login` | string | Username |
| ↳ `id` | number | User ID |
| ↳ `node_id` | string | GraphQL node ID |
| ↳ `avatar_url` | string | Avatar image URL |
| ↳ `url` | string | API URL |
| ↳ `html_url` | string | Profile page URL |
| ↳ `type` | string | User or Organization |
| ↳ `site_admin` | boolean | GitHub staff indicator |
| ↳ `login` | string | Username |
| ↳ `avatar_url` | string | Avatar image URL |
| ↳ `url` | string | API URL |
| ↳ `type` | string | User or Organization |
| ↳ `site_admin` | boolean | GitHub staff indicator |
| ↳ `full_name` | string | Full name \(owner/repo\) |
| ↳ `private` | boolean | Whether repository is private |
| ↳ `html_url` | string | GitHub web URL |
| ↳ `description` | string | Repository description |
| ↳ `owner` | object | Repository owner |
| ↳ `login` | string | Username |
@@ -1765,10 +1697,10 @@ Search for issues and pull requests across GitHub. Use qualifiers like repo:owne
| `total_count` | number | Total matching results |
| `incomplete_results` | boolean | Whether results are incomplete |
| `items` | array | Array of issue/PR objects from GitHub API |
| ↳ `id` | number | Milestone ID |
| ↳ `id` | number | Issue ID |
| ↳ `node_id` | string | GraphQL node ID |
| ↳ `number` | number | Milestone number |
| ↳ `title` | string | Milestone title |
| ↳ `number` | number | Issue number |
| ↳ `title` | string | Title |
| ↳ `state` | string | State \(open or closed\) |
| ↳ `locked` | boolean | Whether issue is locked |
| ↳ `html_url` | string | Web URL |
@@ -1790,10 +1722,6 @@ Search for issues and pull requests across GitHub. Use qualifiers like repo:owne
| ↳ `html_url` | string | Profile page URL |
| ↳ `type` | string | User or Organization |
| ↳ `site_admin` | boolean | GitHub staff indicator |
| ↳ `login` | string | Username |
| ↳ `avatar_url` | string | Avatar image URL |
| ↳ `type` | string | User or Organization |
| ↳ `site_admin` | boolean | GitHub staff indicator |
| ↳ `labels` | array | Issue labels |
| ↳ `id` | number | Label ID |
| ↳ `node_id` | string | GraphQL node ID |
@@ -1802,10 +1730,6 @@ Search for issues and pull requests across GitHub. Use qualifiers like repo:owne
| ↳ `description` | string | Label description |
| ↳ `color` | string | Hex color code |
| ↳ `default` | boolean | Whether this is a default label |
| ↳ `name` | string | Label name |
| ↳ `description` | string | Milestone description |
| ↳ `color` | string | Hex color code |
| ↳ `default` | boolean | Whether this is a default label |
| ↳ `assignee` | object | Primary assignee |
| ↳ `login` | string | Username |
| ↳ `id` | number | User ID |
@@ -1833,14 +1757,11 @@ Search for issues and pull requests across GitHub. Use qualifiers like repo:owne
| ↳ `state` | string | State \(open or closed\) |
| ↳ `html_url` | string | Web URL |
| ↳ `due_on` | string | Due date |
| ↳ `due_on` | string | Due date |
| ↳ `pull_request` | object | Pull request details \(if this is a PR\) |
| ↳ `url` | string | API URL |
| ↳ `html_url` | string | Web URL |
| ↳ `diff_url` | string | Diff URL |
| ↳ `patch_url` | string | Patch URL |
| ↳ `diff_url` | string | Diff URL |
| ↳ `patch_url` | string | Patch URL |
### `github_search_repos`
@@ -1864,13 +1785,13 @@ Search for repositories across GitHub. Use qualifiers like language:python, star
| `total_count` | number | Total matching results |
| `incomplete_results` | boolean | Whether results are incomplete |
| `items` | array | Array of repository objects from GitHub API |
| ↳ `id` | number | User ID |
| ↳ `id` | number | Repository ID |
| ↳ `node_id` | string | GraphQL node ID |
| ↳ `name` | string | License name |
| ↳ `name` | string | Repository name |
| ↳ `full_name` | string | Full name \(owner/repo\) |
| ↳ `private` | boolean | Whether repository is private |
| ↳ `description` | string | Repository description |
| ↳ `html_url` | string | Profile page URL |
| ↳ `html_url` | string | GitHub web URL |
| ↳ `url` | string | API URL |
| ↳ `fork` | boolean | Whether this is a fork |
| ↳ `created_at` | string | Creation timestamp |
@@ -1889,8 +1810,6 @@ Search for repositories across GitHub. Use qualifiers like language:python, star
| ↳ `key` | string | License key \(e.g., mit\) |
| ↳ `name` | string | License name |
| ↳ `spdx_id` | string | SPDX identifier |
| ↳ `key` | string | License key \(e.g., mit\) |
| ↳ `spdx_id` | string | SPDX identifier |
| ↳ `owner` | object | Repository owner |
| ↳ `login` | string | Username |
| ↳ `id` | number | User ID |
@@ -1900,10 +1819,6 @@ Search for repositories across GitHub. Use qualifiers like language:python, star
| ↳ `html_url` | string | Profile page URL |
| ↳ `type` | string | User or Organization |
| ↳ `site_admin` | boolean | GitHub staff indicator |
| ↳ `login` | string | Username |
| ↳ `avatar_url` | string | Avatar image URL |
| ↳ `type` | string | User or Organization |
| ↳ `site_admin` | boolean | GitHub staff indicator |
### `github_search_users`
@@ -1969,22 +1884,19 @@ List commits in a repository with optional filtering by SHA, path, author, commi
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `items` | array | Array of commit objects from GitHub API |
| ↳ `sha` | string | Parent SHA |
| ↳ `sha` | string | Commit SHA |
| ↳ `node_id` | string | GraphQL node ID |
| ↳ `html_url` | string | Parent web URL |
| ↳ `url` | string | Parent API URL |
| ↳ `html_url` | string | Web URL |
| ↳ `url` | string | API URL |
| ↳ `comments_url` | string | Comments API URL |
| ↳ `commit` | object | Core commit data |
| ↳ `url` | string | Tree API URL |
| ↳ `url` | string | Commit API URL |
| ↳ `message` | string | Commit message |
| ↳ `comment_count` | number | Number of comments |
| ↳ `author` | object | Git author |
| ↳ `name` | string | Author name |
| ↳ `email` | string | Author email |
| ↳ `date` | string | Author date \(ISO 8601\) |
| ↳ `name` | string | Committer name |
| ↳ `email` | string | Committer email |
| ↳ `date` | string | Commit date \(ISO 8601\) |
| ↳ `committer` | object | Git committer |
| ↳ `name` | string | Committer name |
| ↳ `email` | string | Committer email |
@@ -1992,18 +1904,11 @@ List commits in a repository with optional filtering by SHA, path, author, commi
| ↳ `tree` | object | Tree object |
| ↳ `sha` | string | Tree SHA |
| ↳ `url` | string | Tree API URL |
| ↳ `sha` | string | Tree SHA |
| ↳ `verification` | object | Signature verification |
| ↳ `verified` | boolean | Whether signature is verified |
| ↳ `reason` | string | Verification reason |
| ↳ `signature` | string | GPG signature |
| ↳ `payload` | string | Signed payload |
| ↳ `verified` | boolean | Whether signature is verified |
| ↳ `reason` | string | Verification reason |
| ↳ `signature` | string | GPG signature |
| ↳ `payload` | string | Signed payload |
| ↳ `message` | string | Commit message |
| ↳ `comment_count` | number | Number of comments |
| ↳ `author` | object | GitHub user \(author\) |
| ↳ `login` | string | Username |
| ↳ `id` | number | User ID |
@@ -2013,9 +1918,6 @@ List commits in a repository with optional filtering by SHA, path, author, commi
| ↳ `html_url` | string | Profile URL |
| ↳ `type` | string | User or Organization |
| ↳ `site_admin` | boolean | GitHub staff indicator |
| ↳ `name` | string | Committer name |
| ↳ `email` | string | Committer email |
| ↳ `date` | string | Commit date \(ISO 8601\) |
| ↳ `committer` | object | GitHub user \(committer\) |
| ↳ `login` | string | Username |
| ↳ `id` | number | User ID |
@@ -2025,23 +1927,6 @@ List commits in a repository with optional filtering by SHA, path, author, commi
| ↳ `html_url` | string | Profile URL |
| ↳ `type` | string | User or Organization |
| ↳ `site_admin` | boolean | GitHub staff indicator |
| ↳ `tree` | object | Tree object |
| ↳ `sha` | string | Tree SHA |
| ↳ `url` | string | Tree API URL |
| ↳ `verification` | object | Signature verification |
| ↳ `verified` | boolean | Whether signature is verified |
| ↳ `reason` | string | Verification reason |
| ↳ `signature` | string | GPG signature |
| ↳ `payload` | string | Signed payload |
| ↳ `verified` | boolean | Whether signature is verified |
| ↳ `reason` | string | Verification reason |
| ↳ `signature` | string | GPG signature |
| ↳ `payload` | string | Signed payload |
| ↳ `login` | string | Username |
| ↳ `id` | number | User ID |
| ↳ `avatar_url` | string | Avatar URL |
| ↳ `type` | string | User or Organization |
| ↳ `site_admin` | boolean | GitHub staff indicator |
| ↳ `parents` | array | Parent commits |
| ↳ `sha` | string | Parent SHA |
| ↳ `url` | string | Parent API URL |
@@ -2071,16 +1956,13 @@ Get detailed information about a specific commit including files changed and sta
| `url` | string | API URL |
| `comments_url` | string | Comments API URL |
| `commit` | object | Core commit data |
| ↳ `url` | string | Tree API URL |
| ↳ `url` | string | Commit API URL |
| ↳ `message` | string | Commit message |
| ↳ `comment_count` | number | Number of comments |
| ↳ `author` | object | Git author |
| ↳ `name` | string | Author name |
| ↳ `email` | string | Author email |
| ↳ `date` | string | Author date \(ISO 8601\) |
| ↳ `name` | string | Committer name |
| ↳ `email` | string | Committer email |
| ↳ `date` | string | Commit date \(ISO 8601\) |
| ↳ `committer` | object | Git committer |
| ↳ `name` | string | Committer name |
| ↳ `email` | string | Committer email |
@@ -2088,16 +1970,11 @@ Get detailed information about a specific commit including files changed and sta
| ↳ `tree` | object | Tree object |
| ↳ `sha` | string | Tree SHA |
| ↳ `url` | string | Tree API URL |
| ↳ `sha` | string | Tree SHA |
| ↳ `verification` | object | Signature verification |
| ↳ `verified` | boolean | Whether signature is verified |
| ↳ `reason` | string | Verification reason |
| ↳ `signature` | string | GPG signature |
| ↳ `payload` | string | Signed payload |
| ↳ `verified` | boolean | Whether signature is verified |
| ↳ `reason` | string | Verification reason |
| ↳ `signature` | string | GPG signature |
| ↳ `payload` | string | Signed payload |
| `author` | object | GitHub user \(author\) |
| ↳ `login` | string | Username |
| ↳ `id` | number | User ID |
@@ -2167,7 +2044,6 @@ Compare two commits or branches to see the diff, commits between them, and chang
| ↳ `message` | string | Commit message |
| ↳ `author` | object | Git author \(name, email, date\) |
| ↳ `committer` | object | Git committer \(name, email, date\) |
| ↳ `message` | string | Commit message |
| ↳ `author` | object | GitHub user \(author\) |
| ↳ `committer` | object | GitHub user \(committer\) |
| `merge_base_commit` | object | Merge base commit object |
@@ -2180,7 +2056,6 @@ Compare two commits or branches to see the diff, commits between them, and chang
| ↳ `message` | string | Commit message |
| ↳ `author` | object | Git author \(name, email, date\) |
| ↳ `committer` | object | Git committer \(name, email, date\) |
| ↳ `message` | string | Commit message |
| ↳ `author` | object | GitHub user |
| ↳ `committer` | object | GitHub user |
| `files` | array | Changed files \(diff entries\) |
@@ -2271,13 +2146,6 @@ Get a gist by ID including its file contents
| `comments_url` | string | Comments API URL |
| `truncated` | boolean | Whether content is truncated |
| `files` | object | Files in the gist \(keyed by filename\) |
| ↳ `filename` | string | File name |
| ↳ `type` | string | MIME type |
| ↳ `language` | string | Programming language |
| ↳ `raw_url` | string | Raw file URL |
| ↳ `size` | number | File size in bytes |
| ↳ `truncated` | boolean | Whether content is truncated |
| ↳ `content` | string | File content |
| `owner` | object | Gist owner |
| ↳ `login` | string | Username |
| ↳ `id` | number | User ID |
@@ -2307,10 +2175,10 @@ List gists for a user or the authenticated user
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `items` | array | Array of gist objects from GitHub API |
| ↳ `id` | number | User ID |
| ↳ `id` | string | Gist ID |
| ↳ `node_id` | string | GraphQL node ID |
| ↳ `url` | string | API URL |
| ↳ `html_url` | string | Profile page URL |
| ↳ `html_url` | string | Web URL |
| ↳ `forks_url` | string | Forks API URL |
| ↳ `commits_url` | string | Commits API URL |
| ↳ `git_pull_url` | string | Git pull URL |
@@ -2332,10 +2200,6 @@ List gists for a user or the authenticated user
| ↳ `html_url` | string | Profile page URL |
| ↳ `type` | string | User or Organization |
| ↳ `site_admin` | boolean | GitHub staff indicator |
| ↳ `login` | string | Username |
| ↳ `avatar_url` | string | Avatar image URL |
| ↳ `type` | string | User or Organization |
| ↳ `site_admin` | boolean | GitHub staff indicator |
| `count` | number | Number of gists returned |
### `github_update_gist`
@@ -2503,14 +2367,13 @@ Fork a repository to your account or an organization
| ↳ `type` | string | User or Organization |
| ↳ `site_admin` | boolean | GitHub staff indicator |
| `parent` | object | Parent repository \(source of the fork\) |
| ↳ `id` | number | User ID |
| ↳ `id` | number | Repository ID |
| ↳ `full_name` | string | Full name |
| ↳ `html_url` | string | Web URL |
| ↳ `description` | string | Description |
| ↳ `owner` | object | Parent owner |
| ↳ `login` | string | Username |
| ↳ `id` | number | User ID |
| ↳ `login` | string | Username |
| `source` | object | Source repository \(ultimate origin\) |
| ↳ `id` | number | Repository ID |
| ↳ `full_name` | string | Full name |
@@ -2536,13 +2399,13 @@ List forks of a repository
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `items` | array | Array of fork repository objects from GitHub API |
| ↳ `id` | number | User ID |
| ↳ `id` | number | Repository ID |
| ↳ `node_id` | string | GraphQL node ID |
| ↳ `name` | string | Repository name |
| ↳ `full_name` | string | Full name \(owner/repo\) |
| ↳ `private` | boolean | Whether repository is private |
| ↳ `description` | string | Repository description |
| ↳ `html_url` | string | Profile page URL |
| ↳ `html_url` | string | GitHub web URL |
| ↳ `url` | string | API URL |
| ↳ `fork` | boolean | Whether this is a fork |
| ↳ `created_at` | string | Creation timestamp |
@@ -2567,10 +2430,6 @@ List forks of a repository
| ↳ `html_url` | string | Profile page URL |
| ↳ `type` | string | User or Organization |
| ↳ `site_admin` | boolean | GitHub staff indicator |
| ↳ `login` | string | Username |
| ↳ `avatar_url` | string | Avatar image URL |
| ↳ `type` | string | User or Organization |
| ↳ `site_admin` | boolean | GitHub staff indicator |
| `count` | number | Number of forks returned |
### `github_create_milestone`
@@ -2667,14 +2526,14 @@ List milestones in a repository
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `items` | array | Array of milestone objects from GitHub API |
| ↳ `id` | number | User ID |
| ↳ `id` | number | Milestone ID |
| ↳ `node_id` | string | GraphQL node ID |
| ↳ `number` | number | Milestone number |
| ↳ `title` | string | Milestone title |
| ↳ `description` | string | Milestone description |
| ↳ `state` | string | State \(open or closed\) |
| ↳ `url` | string | API URL |
| ↳ `html_url` | string | Profile page URL |
| ↳ `html_url` | string | GitHub web URL |
| ↳ `labels_url` | string | Labels API URL |
| ↳ `due_on` | string | Due date \(ISO 8601\) |
| ↳ `open_issues` | number | Number of open issues |
@@ -2691,10 +2550,6 @@ List milestones in a repository
| ↳ `html_url` | string | Profile page URL |
| ↳ `type` | string | User or Organization |
| ↳ `site_admin` | boolean | GitHub staff indicator |
| ↳ `login` | string | Username |
| ↳ `avatar_url` | string | Avatar image URL |
| ↳ `type` | string | User or Organization |
| ↳ `site_admin` | boolean | GitHub staff indicator |
| `count` | number | Number of milestones returned |
### `github_update_milestone`

View File

@@ -139,25 +139,10 @@ Apply multiple updates to a form (add items, update info, change settings, etc.)
| ↳ `title` | string | The form title visible to responders |
| ↳ `description` | string | The form description |
| ↳ `documentTitle` | string | The document title visible in Drive |
| ↳ `title` | string | Item title |
| ↳ `description` | string | Item description |
| ↳ `documentTitle` | string | The document title visible in Drive |
| ↳ `settings` | object | Form settings |
| ↳ `quizSettings` | object | Quiz settings |
| ↳ `isQuiz` | boolean | Whether the form is a quiz |
| ↳ `isQuiz` | boolean | Whether the form is a quiz |
| ↳ `emailCollectionType` | string | Email collection type |
| ↳ `quizSettings` | object | Quiz settings |
| ↳ `isQuiz` | boolean | Whether the form is a quiz |
| ↳ `isQuiz` | boolean | Whether the form is a quiz |
| ↳ `emailCollectionType` | string | Email collection type |
| ↳ `itemId` | string | Item ID |
| ↳ `questionItem` | json | Question item configuration |
| ↳ `questionGroupItem` | json | Question group configuration |
| ↳ `pageBreakItem` | json | Page break configuration |
| ↳ `textItem` | json | Text item configuration |
| ↳ `imageItem` | json | Image item configuration |
| ↳ `videoItem` | json | Video item configuration |
| ↳ `revisionId` | string | The revision ID of the form |
| ↳ `responderUri` | string | The URI to share with responders |
| ↳ `linkedSheetId` | string | The ID of the linked Google Sheet |
@@ -165,13 +150,6 @@ Apply multiple updates to a form (add items, update info, change settings, etc.)
| ↳ `publishState` | object | Current publish state |
| ↳ `isPublished` | boolean | Whether the form is published |
| ↳ `isAcceptingResponses` | boolean | Whether the form is accepting responses |
| ↳ `isPublished` | boolean | Whether the form is published |
| ↳ `isAcceptingResponses` | boolean | Whether the form is accepting responses |
| ↳ `publishState` | object | Current publish state |
| ↳ `isPublished` | boolean | Whether the form is published |
| ↳ `isAcceptingResponses` | boolean | Whether the form is accepting responses |
| ↳ `isPublished` | boolean | Whether the form is published |
| ↳ `isAcceptingResponses` | boolean | Whether the form is accepting responses |
### `google_forms_set_publish_settings`
@@ -194,8 +172,6 @@ Update the publish settings of a form (publish/unpublish, accept responses)
| ↳ `publishState` | object | The publish state |
| ↳ `isPublished` | boolean | Whether the form is published |
| ↳ `isAcceptingResponses` | boolean | Whether the form accepts responses |
| ↳ `isPublished` | boolean | Whether the form is published |
| ↳ `isAcceptingResponses` | boolean | Whether the form accepts responses |
### `google_forms_create_watch`

View File

@@ -57,8 +57,6 @@ Read content from a Google Slides presentation
| ↳ `pageSize` | object | Presentation page size |
| ↳ `width` | json | Page width as a Dimension object |
| ↳ `height` | json | Page height as a Dimension object |
| ↳ `width` | json | Page width as a Dimension object |
| ↳ `height` | json | Page height as a Dimension object |
| ↳ `mimeType` | string | The mime type of the presentation |
| ↳ `url` | string | URL to open the presentation |

View File

@@ -60,8 +60,5 @@ Generate completions using Hugging Face Inference API
| ↳ `prompt_tokens` | number | Number of tokens in the prompt |
| ↳ `completion_tokens` | number | Number of tokens in the completion |
| ↳ `total_tokens` | number | Total number of tokens used |
| ↳ `prompt_tokens` | number | Number of tokens in the prompt |
| ↳ `completion_tokens` | number | Number of tokens in the completion |
| ↳ `total_tokens` | number | Total number of tokens used |

View File

@@ -62,6 +62,5 @@ Generate images using OpenAI
| ↳ `image` | string | Base64 encoded image data |
| ↳ `metadata` | object | Image generation metadata |
| ↳ `model` | string | Model used for image generation |
| ↳ `model` | string | Model used for image generation |

View File

@@ -57,8 +57,8 @@ List incidents from incident.io. Returns a list of incidents with their details
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `incidents` | array | List of incidents |
| ↳ `id` | string | Type ID |
| ↳ `name` | string | Type name |
| ↳ `id` | string | Incident ID |
| ↳ `name` | string | Incident name |
| ↳ `summary` | string | Brief summary of the incident |
| ↳ `description` | string | Detailed description of the incident |
| ↳ `mode` | string | Incident mode \(e.g., standard, retrospective\) |
@@ -67,12 +67,10 @@ List incidents from incident.io. Returns a list of incidents with their details
| ↳ `id` | string | Severity ID |
| ↳ `name` | string | Severity name |
| ↳ `rank` | number | Severity rank |
| ↳ `rank` | number | Severity rank |
| ↳ `status` | object | Current status of the incident |
| ↳ `id` | string | Status ID |
| ↳ `name` | string | Status name |
| ↳ `category` | string | Status category |
| ↳ `category` | string | Status category |
| ↳ `incident_type` | object | Type of the incident |
| ↳ `id` | string | Type ID |
| ↳ `name` | string | Type name |
@@ -109,8 +107,8 @@ Create a new incident in incident.io. Requires idempotency_key, severity_id, and
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `incident` | object | The created incident object |
| ↳ `id` | string | Type ID |
| ↳ `name` | string | Type name |
| ↳ `id` | string | Incident ID |
| ↳ `name` | string | Incident name |
| ↳ `summary` | string | Brief summary of the incident |
| ↳ `description` | string | Detailed description of the incident |
| ↳ `mode` | string | Incident mode \(e.g., standard, retrospective\) |
@@ -119,12 +117,10 @@ Create a new incident in incident.io. Requires idempotency_key, severity_id, and
| ↳ `id` | string | Severity ID |
| ↳ `name` | string | Severity name |
| ↳ `rank` | number | Severity rank |
| ↳ `rank` | number | Severity rank |
| ↳ `status` | object | Current status of the incident |
| ↳ `id` | string | Status ID |
| ↳ `name` | string | Status name |
| ↳ `category` | string | Status category |
| ↳ `category` | string | Status category |
| ↳ `incident_type` | object | Type of the incident |
| ↳ `id` | string | Type ID |
| ↳ `name` | string | Type name |
@@ -151,8 +147,8 @@ Retrieve detailed information about a specific incident from incident.io by its
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `incident` | object | Detailed incident information |
| ↳ `id` | string | Type ID |
| ↳ `name` | string | Type name |
| ↳ `id` | string | Incident ID |
| ↳ `name` | string | Incident name |
| ↳ `summary` | string | Brief summary of the incident |
| ↳ `description` | string | Detailed description of the incident |
| ↳ `mode` | string | Incident mode \(e.g., standard, retrospective\) |
@@ -162,12 +158,10 @@ Retrieve detailed information about a specific incident from incident.io by its
| ↳ `id` | string | Severity ID |
| ↳ `name` | string | Severity name |
| ↳ `rank` | number | Severity rank |
| ↳ `rank` | number | Severity rank |
| ↳ `status` | object | Current status of the incident |
| ↳ `id` | string | Status ID |
| ↳ `name` | string | Status name |
| ↳ `category` | string | Status category |
| ↳ `category` | string | Status category |
| ↳ `incident_type` | object | Type of the incident |
| ↳ `id` | string | Type ID |
| ↳ `name` | string | Type name |
@@ -202,8 +196,8 @@ Update an existing incident in incident.io. Can update name, summary, severity,
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `incident` | object | The updated incident object |
| ↳ `id` | string | Type ID |
| ↳ `name` | string | Type name |
| ↳ `id` | string | Incident ID |
| ↳ `name` | string | Incident name |
| ↳ `summary` | string | Brief summary of the incident |
| ↳ `description` | string | Detailed description of the incident |
| ↳ `mode` | string | Incident mode \(e.g., standard, retrospective\) |
@@ -212,12 +206,10 @@ Update an existing incident in incident.io. Can update name, summary, severity,
| ↳ `id` | string | Severity ID |
| ↳ `name` | string | Severity name |
| ↳ `rank` | number | Severity rank |
| ↳ `rank` | number | Severity rank |
| ↳ `status` | object | Current status of the incident |
| ↳ `id` | string | Status ID |
| ↳ `name` | string | Status name |
| ↳ `category` | string | Status category |
| ↳ `category` | string | Status category |
| ↳ `incident_type` | object | Type of the incident |
| ↳ `id` | string | Type ID |
| ↳ `name` | string | Type name |
@@ -245,14 +237,12 @@ List actions from incident.io. Optionally filter by incident ID.
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `actions` | array | List of actions |
| ↳ `id` | string | User ID |
| ↳ `id` | string | Action ID |
| ↳ `description` | string | Action description |
| ↳ `assignee` | object | Assigned user |
| ↳ `id` | string | User ID |
| ↳ `name` | string | User name |
| ↳ `email` | string | User email |
| ↳ `name` | string | User name |
| ↳ `email` | string | User email |
| ↳ `status` | string | Action status |
| ↳ `due_at` | string | Due date/time |
| ↳ `created_at` | string | Creation timestamp |
@@ -267,9 +257,6 @@ List actions from incident.io. Optionally filter by incident ID.
| ↳ `provider` | string | Issue tracking provider \(e.g., Jira, Linear\) |
| ↳ `issue_name` | string | Issue identifier |
| ↳ `issue_permalink` | string | URL to the external issue |
| ↳ `provider` | string | Issue tracking provider \(e.g., Jira, Linear\) |
| ↳ `issue_name` | string | Issue identifier |
| ↳ `issue_permalink` | string | URL to the external issue |
### `incidentio_actions_show`
@@ -287,14 +274,12 @@ Get detailed information about a specific action from incident.io.
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `action` | object | Action details |
| ↳ `id` | string | User ID |
| ↳ `id` | string | Action ID |
| ↳ `description` | string | Action description |
| ↳ `assignee` | object | Assigned user |
| ↳ `id` | string | User ID |
| ↳ `name` | string | User name |
| ↳ `email` | string | User email |
| ↳ `name` | string | User name |
| ↳ `email` | string | User email |
| ↳ `status` | string | Action status |
| ↳ `due_at` | string | Due date/time |
| ↳ `created_at` | string | Creation timestamp |
@@ -309,9 +294,6 @@ Get detailed information about a specific action from incident.io.
| ↳ `provider` | string | Issue tracking provider \(e.g., Jira, Linear\) |
| ↳ `issue_name` | string | Issue identifier |
| ↳ `issue_permalink` | string | URL to the external issue |
| ↳ `provider` | string | Issue tracking provider \(e.g., Jira, Linear\) |
| ↳ `issue_name` | string | Issue identifier |
| ↳ `issue_permalink` | string | URL to the external issue |
### `incidentio_follow_ups_list`
@@ -330,22 +312,19 @@ List follow-ups from incident.io. Optionally filter by incident ID.
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `follow_ups` | array | List of follow-ups |
| ↳ `id` | string | User ID |
| ↳ `id` | string | Follow-up ID |
| ↳ `title` | string | Follow-up title |
| ↳ `description` | string | Priority description |
| ↳ `description` | string | Follow-up description |
| ↳ `assignee` | object | Assigned user |
| ↳ `id` | string | User ID |
| ↳ `name` | string | User name |
| ↳ `email` | string | User email |
| ↳ `name` | string | User name |
| ↳ `email` | string | User email |
| ↳ `status` | string | Follow-up status |
| ↳ `priority` | object | Follow-up priority |
| ↳ `id` | string | Priority ID |
| ↳ `name` | string | Priority name |
| ↳ `description` | string | Priority description |
| ↳ `rank` | number | Priority rank |
| ↳ `rank` | number | Priority rank |
| ↳ `created_at` | string | Creation timestamp |
| ↳ `updated_at` | string | Last update timestamp |
| ↳ `incident_id` | string | Associated incident ID |
@@ -359,9 +338,6 @@ List follow-ups from incident.io. Optionally filter by incident ID.
| ↳ `provider` | string | External provider name |
| ↳ `issue_name` | string | External issue name or ID |
| ↳ `issue_permalink` | string | Permalink to external issue |
| ↳ `provider` | string | External provider name |
| ↳ `issue_name` | string | External issue name or ID |
| ↳ `issue_permalink` | string | Permalink to external issue |
### `incidentio_follow_ups_show`
@@ -379,22 +355,19 @@ Get detailed information about a specific follow-up from incident.io.
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `follow_up` | object | Follow-up details |
| ↳ `id` | string | User ID |
| ↳ `id` | string | Follow-up ID |
| ↳ `title` | string | Follow-up title |
| ↳ `description` | string | Priority description |
| ↳ `description` | string | Follow-up description |
| ↳ `assignee` | object | Assigned user |
| ↳ `id` | string | User ID |
| ↳ `name` | string | User name |
| ↳ `email` | string | User email |
| ↳ `name` | string | User name |
| ↳ `email` | string | User email |
| ↳ `status` | string | Follow-up status |
| ↳ `priority` | object | Follow-up priority |
| ↳ `id` | string | Priority ID |
| ↳ `name` | string | Priority name |
| ↳ `description` | string | Priority description |
| ↳ `rank` | number | Priority rank |
| ↳ `rank` | number | Priority rank |
| ↳ `created_at` | string | Creation timestamp |
| ↳ `updated_at` | string | Last update timestamp |
| ↳ `incident_id` | string | Associated incident ID |
@@ -408,9 +381,6 @@ Get detailed information about a specific follow-up from incident.io.
| ↳ `provider` | string | External provider name |
| ↳ `issue_name` | string | External issue name or ID |
| ↳ `issue_permalink` | string | Permalink to external issue |
| ↳ `provider` | string | External provider name |
| ↳ `issue_name` | string | External issue name or ID |
| ↳ `issue_permalink` | string | Permalink to external issue |
### `incidentio_users_list`
@@ -1089,25 +1059,21 @@ List all updates for a specific incident in incident.io
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `incident_updates` | array | List of incident updates |
| ↳ `id` | string | User ID |
| ↳ `id` | string | The update ID |
| ↳ `incident_id` | string | The incident ID |
| ↳ `message` | string | The update message |
| ↳ `new_severity` | object | New severity if changed |
| ↳ `id` | string | Severity ID |
| ↳ `name` | string | Severity name |
| ↳ `rank` | number | Severity rank |
| ↳ `name` | string | User name |
| ↳ `rank` | number | Severity rank |
| ↳ `new_status` | object | New status if changed |
| ↳ `id` | string | Status ID |
| ↳ `name` | string | Status name |
| ↳ `category` | string | Status category |
| ↳ `category` | string | Status category |
| ↳ `updater` | object | User who created the update |
| ↳ `id` | string | User ID |
| ↳ `name` | string | User name |
| ↳ `email` | string | User email |
| ↳ `email` | string | User email |
| ↳ `created_at` | string | When the update was created |
| ↳ `updated_at` | string | When the update was last modified |
| `pagination_meta` | object | Pagination information |
@@ -1134,14 +1100,12 @@ List all entries for a specific schedule in incident.io
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `schedule_entries` | array | List of schedule entries |
| ↳ `id` | string | User ID |
| ↳ `id` | string | The entry ID |
| ↳ `schedule_id` | string | The schedule ID |
| ↳ `user` | object | User assigned to this entry |
| ↳ `id` | string | User ID |
| ↳ `name` | string | User name |
| ↳ `email` | string | User email |
| ↳ `name` | string | User name |
| ↳ `email` | string | User email |
| ↳ `start_at` | string | When the entry starts |
| ↳ `end_at` | string | When the entry ends |
| ↳ `layer_id` | string | The schedule layer ID |
@@ -1174,15 +1138,13 @@ Create a new schedule override in incident.io
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `override` | object | The created schedule override |
| ↳ `id` | string | User ID |
| ↳ `id` | string | The override ID |
| ↳ `rotation_id` | string | The rotation ID |
| ↳ `schedule_id` | string | The schedule ID |
| ↳ `user` | object | User assigned to this override |
| ↳ `id` | string | User ID |
| ↳ `name` | string | User name |
| ↳ `email` | string | User email |
| ↳ `name` | string | User name |
| ↳ `email` | string | User email |
| ↳ `start_at` | string | When the override starts |
| ↳ `end_at` | string | When the override ends |
| ↳ `created_at` | string | When the override was created |
@@ -1206,7 +1168,7 @@ Create a new escalation path in incident.io
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `escalation_path` | object | The created escalation path |
| ↳ `id` | string | Target ID |
| ↳ `id` | string | The escalation path ID |
| ↳ `name` | string | The escalation path name |
| ↳ `path` | array | Array of escalation levels |
| ↳ `targets` | array | Targets for this level |
@@ -1215,30 +1177,11 @@ Create a new escalation path in incident.io
| ↳ `schedule_id` | string | Schedule ID if type is schedule |
| ↳ `user_id` | string | User ID if type is user |
| ↳ `urgency` | string | Urgency level |
| ↳ `id` | string | Target ID |
| ↳ `type` | string | Target type |
| ↳ `schedule_id` | string | Schedule ID if type is schedule |
| ↳ `user_id` | string | User ID if type is user |
| ↳ `urgency` | string | Urgency level |
| ↳ `time_to_ack_seconds` | number | Time to acknowledge in seconds |
| ↳ `targets` | array | Targets for this level |
| ↳ `id` | string | Target ID |
| ↳ `type` | string | Target type |
| ↳ `schedule_id` | string | Schedule ID if type is schedule |
| ↳ `user_id` | string | User ID if type is user |
| ↳ `urgency` | string | Urgency level |
| ↳ `type` | string | Target type |
| ↳ `schedule_id` | string | Schedule ID if type is schedule |
| ↳ `user_id` | string | User ID if type is user |
| ↳ `urgency` | string | Urgency level |
| ↳ `time_to_ack_seconds` | number | Time to acknowledge in seconds |
| ↳ `working_hours` | array | Working hours configuration |
| ↳ `weekday` | string | Day of week |
| ↳ `start_time` | string | Start time |
| ↳ `end_time` | string | End time |
| ↳ `weekday` | string | Day of week |
| ↳ `start_time` | string | Start time |
| ↳ `end_time` | string | End time |
| ↳ `created_at` | string | When the path was created |
| ↳ `updated_at` | string | When the path was last updated |
@@ -1258,7 +1201,7 @@ Get details of a specific escalation path in incident.io
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `escalation_path` | object | The escalation path details |
| ↳ `id` | string | Target ID |
| ↳ `id` | string | The escalation path ID |
| ↳ `name` | string | The escalation path name |
| ↳ `path` | array | Array of escalation levels |
| ↳ `targets` | array | Targets for this level |
@@ -1267,30 +1210,11 @@ Get details of a specific escalation path in incident.io
| ↳ `schedule_id` | string | Schedule ID if type is schedule |
| ↳ `user_id` | string | User ID if type is user |
| ↳ `urgency` | string | Urgency level |
| ↳ `id` | string | Target ID |
| ↳ `type` | string | Target type |
| ↳ `schedule_id` | string | Schedule ID if type is schedule |
| ↳ `user_id` | string | User ID if type is user |
| ↳ `urgency` | string | Urgency level |
| ↳ `time_to_ack_seconds` | number | Time to acknowledge in seconds |
| ↳ `targets` | array | Targets for this level |
| ↳ `id` | string | Target ID |
| ↳ `type` | string | Target type |
| ↳ `schedule_id` | string | Schedule ID if type is schedule |
| ↳ `user_id` | string | User ID if type is user |
| ↳ `urgency` | string | Urgency level |
| ↳ `type` | string | Target type |
| ↳ `schedule_id` | string | Schedule ID if type is schedule |
| ↳ `user_id` | string | User ID if type is user |
| ↳ `urgency` | string | Urgency level |
| ↳ `time_to_ack_seconds` | number | Time to acknowledge in seconds |
| ↳ `working_hours` | array | Working hours configuration |
| ↳ `weekday` | string | Day of week |
| ↳ `start_time` | string | Start time |
| ↳ `end_time` | string | End time |
| ↳ `weekday` | string | Day of week |
| ↳ `start_time` | string | Start time |
| ↳ `end_time` | string | End time |
| ↳ `created_at` | string | When the path was created |
| ↳ `updated_at` | string | When the path was last updated |
@@ -1313,7 +1237,7 @@ Update an existing escalation path in incident.io
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `escalation_path` | object | The updated escalation path |
| ↳ `id` | string | Target ID |
| ↳ `id` | string | The escalation path ID |
| ↳ `name` | string | The escalation path name |
| ↳ `path` | array | Array of escalation levels |
| ↳ `targets` | array | Targets for this level |
@@ -1322,30 +1246,11 @@ Update an existing escalation path in incident.io
| ↳ `schedule_id` | string | Schedule ID if type is schedule |
| ↳ `user_id` | string | User ID if type is user |
| ↳ `urgency` | string | Urgency level |
| ↳ `id` | string | Target ID |
| ↳ `type` | string | Target type |
| ↳ `schedule_id` | string | Schedule ID if type is schedule |
| ↳ `user_id` | string | User ID if type is user |
| ↳ `urgency` | string | Urgency level |
| ↳ `time_to_ack_seconds` | number | Time to acknowledge in seconds |
| ↳ `targets` | array | Targets for this level |
| ↳ `id` | string | Target ID |
| ↳ `type` | string | Target type |
| ↳ `schedule_id` | string | Schedule ID if type is schedule |
| ↳ `user_id` | string | User ID if type is user |
| ↳ `urgency` | string | Urgency level |
| ↳ `type` | string | Target type |
| ↳ `schedule_id` | string | Schedule ID if type is schedule |
| ↳ `user_id` | string | User ID if type is user |
| ↳ `urgency` | string | Urgency level |
| ↳ `time_to_ack_seconds` | number | Time to acknowledge in seconds |
| ↳ `working_hours` | array | Working hours configuration |
| ↳ `weekday` | string | Day of week |
| ↳ `start_time` | string | Start time |
| ↳ `end_time` | string | End time |
| ↳ `weekday` | string | Day of week |
| ↳ `start_time` | string | Start time |
| ↳ `end_time` | string | End time |
| ↳ `created_at` | string | When the path was created |
| ↳ `updated_at` | string | When the path was last updated |

View File

@@ -62,7 +62,7 @@ Create a new contact in Intercom with email, external_id, or role. Returns API-a
| --------- | ---- | ----------- |
| `contact` | object | Created contact object |
| ↳ `id` | string | Unique identifier for the contact |
| ↳ `type` | string | List type |
| ↳ `type` | string | Object type \(contact\) |
| ↳ `role` | string | Role of the contact \(user or lead\) |
| ↳ `email` | string | Email address of the contact |
| ↳ `phone` | string | Phone number of the contact |
@@ -82,10 +82,6 @@ Create a new contact in Intercom with email, external_id, or role. Returns API-a
| ↳ `data` | array | Array of tag objects |
| ↳ `has_more` | boolean | Whether there are more tags |
| ↳ `total_count` | number | Total number of tags |
| ↳ `url` | string | URL to fetch companies |
| ↳ `data` | array | Array of social profile objects |
| ↳ `has_more` | boolean | Whether there are more companies |
| ↳ `total_count` | number | Total number of companies |
| ↳ `notes` | object | Notes associated with the contact |
| ↳ `type` | string | List type |
| ↳ `url` | string | URL to fetch notes |
@@ -105,11 +101,6 @@ Create a new contact in Intercom with email, external_id, or role. Returns API-a
| ↳ `country` | string | Country |
| ↳ `country_code` | string | Country code |
| ↳ `continent_code` | string | Continent code |
| ↳ `city` | string | City |
| ↳ `region` | string | Region/State |
| ↳ `country` | string | Country |
| ↳ `country_code` | string | Country code |
| ↳ `continent_code` | string | Continent code |
| ↳ `social_profiles` | object | Social profiles of the contact |
| ↳ `type` | string | List type |
| ↳ `data` | array | Array of social profile objects |
@@ -323,7 +314,7 @@ Create or update a company in Intercom
| --------- | ---- | ----------- |
| `company` | object | Created or updated company object |
| ↳ `id` | string | Unique identifier for the company |
| ↳ `type` | string | Segment list type |
| ↳ `type` | string | Object type \(company\) |
| ↳ `app_id` | string | Intercom app ID |
| ↳ `company_id` | string | Your unique identifier for the company |
| ↳ `name` | string | Name of the company |
@@ -338,8 +329,12 @@ Create or update a company in Intercom
| ↳ `updated_at` | number | Unix timestamp when company was last updated |
| ↳ `remote_created_at` | number | Unix timestamp when company was created by you |
| ↳ `custom_attributes` | object | Custom attributes set on the company |
| ↳ `tags` | array | Array of tag objects |
| ↳ `segments` | array | Array of segment objects |
| ↳ `tags` | object | Tags associated with the company |
| ↳ `type` | string | Tag list type |
| ↳ `tags` | array | Array of tag objects |
| ↳ `segments` | object | Segments the company belongs to |
| ↳ `type` | string | Segment list type |
| ↳ `segments` | array | Array of segment objects |
| `companyId` | string | ID of the created/updated company |
### `intercom_get_company`

View File

@@ -12,6 +12,7 @@
"calendly",
"circleback",
"clay",
"clerk",
"confluence",
"cursor",
"datadog",

View File

@@ -64,19 +64,10 @@ Parse PDF documents using Mistral OCR API
| ↳ `bottom_right_x` | number | Bottom-right X coordinate in pixels |
| ↳ `bottom_right_y` | number | Bottom-right Y coordinate in pixels |
| ↳ `image_base64` | string | Base64-encoded image data \(when include_image_base64=true\) |
| ↳ `id` | string | Image identifier \(e.g., img-0.jpeg\) |
| ↳ `top_left_x` | number | Top-left X coordinate in pixels |
| ↳ `top_left_y` | number | Top-left Y coordinate in pixels |
| ↳ `bottom_right_x` | number | Bottom-right X coordinate in pixels |
| ↳ `bottom_right_y` | number | Bottom-right Y coordinate in pixels |
| ↳ `image_base64` | string | Base64-encoded image data \(when include_image_base64=true\) |
| ↳ `dimensions` | object | Page dimensions |
| ↳ `dpi` | number | Dots per inch |
| ↳ `height` | number | Page height in pixels |
| ↳ `width` | number | Page width in pixels |
| ↳ `dpi` | number | Dots per inch |
| ↳ `height` | number | Page height in pixels |
| ↳ `width` | number | Page width in pixels |
| ↳ `tables` | array | Extracted tables as HTML/markdown \(when table_format is set\). Referenced via placeholders like \[tbl-0.html\] |
| ↳ `hyperlinks` | array | Array of URL strings detected in the page \(e.g., \[ |
| ↳ `header` | string | Page header content \(when extract_header=true\) |

View File

@@ -59,7 +59,5 @@ Generate embeddings from text using OpenAI
| ↳ `usage` | object | Token usage information |
| ↳ `prompt_tokens` | number | Number of tokens in the prompt |
| ↳ `total_tokens` | number | Total number of tokens used |
| ↳ `prompt_tokens` | number | Number of tokens in the prompt |
| ↳ `total_tokens` | number | Total number of tokens used |

View File

@@ -112,9 +112,6 @@ Conduct comprehensive deep research across the web using Parallel AI. Synthesize
| ↳ `url` | string | Source URL |
| ↳ `title` | string | Source title |
| ↳ `excerpts` | array | Relevant excerpts from the source |
| ↳ `url` | string | Source URL |
| ↳ `title` | string | Source title |
| ↳ `excerpts` | array | Relevant excerpts from the source |
| ↳ `confidence` | string | Confidence level indicator |

View File

@@ -323,8 +323,6 @@ Retrieve the order book summary for a specific token
| ↳ `bids` | array | Bid orders |
| ↳ `price` | string | Bid price |
| ↳ `size` | string | Bid size |
| ↳ `price` | string | Ask price |
| ↳ `size` | string | Ask size |
| ↳ `asks` | array | Ask orders |
| ↳ `price` | string | Ask price |
| ↳ `size` | string | Ask size |
@@ -637,15 +635,5 @@ Retrieve top holders of a specific market token
| ↳ `name` | string | Holder display name |
| ↳ `profileImage` | string | Profile image URL |
| ↳ `profileImageOptimized` | string | Optimized profile image URL |
| ↳ `proxyWallet` | string | Holder wallet address |
| ↳ `bio` | string | Holder bio |
| ↳ `asset` | string | Asset ID |
| ↳ `pseudonym` | string | Holder pseudonym |
| ↳ `amount` | number | Amount held |
| ↳ `displayUsernamePublic` | boolean | Whether username is publicly displayed |
| ↳ `outcomeIndex` | number | Outcome index |
| ↳ `name` | string | Holder display name |
| ↳ `profileImage` | string | Profile image URL |
| ↳ `profileImageOptimized` | string | Optimized profile image URL |

View File

@@ -58,14 +58,9 @@ Retrieve accounts from Salesforce CRM
| ↳ `nextRecordsUrl` | string | URL for next page of results |
| ↳ `totalSize` | number | Total number of records |
| ↳ `done` | boolean | Whether all records returned |
| ↳ `nextRecordsUrl` | string | URL for next page of results |
| ↳ `totalSize` | number | Total number of records |
| ↳ `done` | boolean | Whether all records returned |
| ↳ `metadata` | object | Response metadata |
| ↳ `totalReturned` | number | Number of accounts returned |
| ↳ `hasMore` | boolean | Whether more records exist |
| ↳ `totalReturned` | number | Number of accounts returned |
| ↳ `hasMore` | boolean | Whether more records exist |
| ↳ `success` | boolean | Salesforce operation success |
### `salesforce_create_account`
@@ -184,14 +179,9 @@ Get contact(s) from Salesforce - single contact if ID provided, or list if not
| ↳ `nextRecordsUrl` | string | URL for next page of results |
| ↳ `totalSize` | number | Total number of records |
| ↳ `done` | boolean | Whether all records returned |
| ↳ `nextRecordsUrl` | string | URL for next page of results |
| ↳ `totalSize` | number | Total number of records |
| ↳ `done` | boolean | Whether all records returned |
| ↳ `metadata` | object | Response metadata |
| ↳ `totalReturned` | number | Number of contacts returned |
| ↳ `hasMore` | boolean | Whether more records exist |
| ↳ `totalReturned` | number | Number of contacts returned |
| ↳ `hasMore` | boolean | Whether more records exist |
| ↳ `singleContact` | boolean | Whether single contact was returned |
| ↳ `success` | boolean | Salesforce operation success |
@@ -311,14 +301,9 @@ Get lead(s) from Salesforce
| ↳ `nextRecordsUrl` | string | URL for next page of results |
| ↳ `totalSize` | number | Total number of records |
| ↳ `done` | boolean | Whether all records returned |
| ↳ `nextRecordsUrl` | string | URL for next page of results |
| ↳ `totalSize` | number | Total number of records |
| ↳ `done` | boolean | Whether all records returned |
| ↳ `metadata` | object | Response metadata |
| ↳ `totalReturned` | number | Number of leads returned |
| ↳ `hasMore` | boolean | Whether more records exist |
| ↳ `totalReturned` | number | Number of leads returned |
| ↳ `hasMore` | boolean | Whether more records exist |
| ↳ `singleLead` | boolean | Whether single lead was returned |
| ↳ `success` | boolean | Operation success status |
@@ -430,14 +415,9 @@ Get opportunity(ies) from Salesforce
| ↳ `nextRecordsUrl` | string | URL for next page of results |
| ↳ `totalSize` | number | Total number of records |
| ↳ `done` | boolean | Whether all records returned |
| ↳ `nextRecordsUrl` | string | URL for next page of results |
| ↳ `totalSize` | number | Total number of records |
| ↳ `done` | boolean | Whether all records returned |
| ↳ `metadata` | object | Response metadata |
| ↳ `totalReturned` | number | Number of opportunities returned |
| ↳ `hasMore` | boolean | Whether more records exist |
| ↳ `totalReturned` | number | Number of opportunities returned |
| ↳ `hasMore` | boolean | Whether more records exist |
| ↳ `success` | boolean | Operation success status |
### `salesforce_create_opportunity`
@@ -544,14 +524,9 @@ Get case(s) from Salesforce
| ↳ `nextRecordsUrl` | string | URL for next page of results |
| ↳ `totalSize` | number | Total number of records |
| ↳ `done` | boolean | Whether all records returned |
| ↳ `nextRecordsUrl` | string | URL for next page of results |
| ↳ `totalSize` | number | Total number of records |
| ↳ `done` | boolean | Whether all records returned |
| ↳ `metadata` | object | Response metadata |
| ↳ `totalReturned` | number | Number of cases returned |
| ↳ `hasMore` | boolean | Whether more records exist |
| ↳ `totalReturned` | number | Number of cases returned |
| ↳ `hasMore` | boolean | Whether more records exist |
| ↳ `success` | boolean | Operation success status |
### `salesforce_create_case`
@@ -655,14 +630,9 @@ Get task(s) from Salesforce
| ↳ `nextRecordsUrl` | string | URL for next page of results |
| ↳ `totalSize` | number | Total number of records |
| ↳ `done` | boolean | Whether all records returned |
| ↳ `nextRecordsUrl` | string | URL for next page of results |
| ↳ `totalSize` | number | Total number of records |
| ↳ `done` | boolean | Whether all records returned |
| ↳ `metadata` | object | Response metadata |
| ↳ `totalReturned` | number | Number of tasks returned |
| ↳ `hasMore` | boolean | Whether more records exist |
| ↳ `totalReturned` | number | Number of tasks returned |
| ↳ `hasMore` | boolean | Whether more records exist |
| ↳ `success` | boolean | Operation success status |
### `salesforce_create_task`
@@ -938,8 +908,6 @@ Execute a custom SOQL query to retrieve data from Salesforce
| ↳ `metadata` | object | Response metadata |
| ↳ `totalReturned` | number | Number of records returned in this response |
| ↳ `hasMore` | boolean | Whether more records exist |
| ↳ `totalReturned` | number | Number of records returned in this response |
| ↳ `hasMore` | boolean | Whether more records exist |
| ↳ `success` | boolean | Salesforce operation success |
### `salesforce_query_more`
@@ -967,8 +935,6 @@ Retrieve additional query results using the nextRecordsUrl from a previous query
| ↳ `metadata` | object | Response metadata |
| ↳ `totalReturned` | number | Number of records returned in this response |
| ↳ `hasMore` | boolean | Whether more records exist |
| ↳ `totalReturned` | number | Number of records returned in this response |
| ↳ `hasMore` | boolean | Whether more records exist |
| ↳ `success` | boolean | Salesforce operation success |
### `salesforce_describe_object`

View File

@@ -68,7 +68,7 @@ List issues from Sentry for a specific organization and optionally a specific pr
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `issues` | array | List of Sentry issues |
| ↳ `id` | string | User ID |
| ↳ `id` | string | Unique issue ID |
| ↳ `shortId` | string | Short issue identifier |
| ↳ `title` | string | Issue title |
| ↳ `culprit` | string | Function or location that caused the issue |
@@ -78,27 +78,22 @@ List issues from Sentry for a specific organization and optionally a specific pr
| ↳ `status` | string | Current issue status |
| ↳ `statusDetails` | object | Additional details about the status |
| ↳ `isPublic` | boolean | Whether the issue is publicly visible |
| ↳ `platform` | string | Project platform |
| ↳ `platform` | string | Platform where the issue occurred |
| ↳ `project` | object | Project information |
| ↳ `id` | string | Project ID |
| ↳ `name` | string | Project name |
| ↳ `slug` | string | Project slug |
| ↳ `platform` | string | Project platform |
| ↳ `name` | string | User name |
| ↳ `slug` | string | Project slug |
| ↳ `type` | string | Type of error \(e.g., TypeError\) |
| ↳ `type` | string | Issue type |
| ↳ `metadata` | object | Error metadata |
| ↳ `type` | string | Type of error \(e.g., TypeError\) |
| ↳ `value` | string | Error message or value |
| ↳ `function` | string | Function where the error occurred |
| ↳ `value` | string | Error message or value |
| ↳ `function` | string | Function where the error occurred |
| ↳ `numComments` | number | Number of comments on the issue |
| ↳ `assignedTo` | object | User assigned to the issue |
| ↳ `id` | string | User ID |
| ↳ `name` | string | User name |
| ↳ `email` | string | User email |
| ↳ `email` | string | User email |
| ↳ `isBookmarked` | boolean | Whether the issue is bookmarked |
| ↳ `isSubscribed` | boolean | Whether subscribed to updates |
| ↳ `hasSeen` | boolean | Whether the user has seen this issue |
@@ -130,7 +125,7 @@ Retrieve detailed information about a specific Sentry issue by its ID. Returns c
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `issue` | object | Detailed information about the Sentry issue |
| ↳ `id` | string | User ID |
| ↳ `id` | string | Unique issue ID |
| ↳ `shortId` | string | Short issue identifier |
| ↳ `title` | string | Issue title |
| ↳ `culprit` | string | Function or location that caused the issue |
@@ -140,27 +135,22 @@ Retrieve detailed information about a specific Sentry issue by its ID. Returns c
| ↳ `status` | string | Current issue status |
| ↳ `statusDetails` | object | Additional details about the status |
| ↳ `isPublic` | boolean | Whether the issue is publicly visible |
| ↳ `platform` | string | Project platform |
| ↳ `platform` | string | Platform where the issue occurred |
| ↳ `project` | object | Project information |
| ↳ `id` | string | Project ID |
| ↳ `name` | string | Project name |
| ↳ `slug` | string | Project slug |
| ↳ `platform` | string | Project platform |
| ↳ `name` | string | User name |
| ↳ `slug` | string | Project slug |
| ↳ `type` | string | Type of error \(e.g., TypeError, ValueError\) |
| ↳ `type` | string | Issue type |
| ↳ `metadata` | object | Error metadata |
| ↳ `type` | string | Type of error \(e.g., TypeError, ValueError\) |
| ↳ `value` | string | Error message or value |
| ↳ `function` | string | Function where the error occurred |
| ↳ `value` | string | Error message or value |
| ↳ `function` | string | Function where the error occurred |
| ↳ `numComments` | number | Number of comments on the issue |
| ↳ `assignedTo` | object | User assigned to the issue \(if any\) |
| ↳ `id` | string | User ID |
| ↳ `name` | string | User name |
| ↳ `email` | string | User email |
| ↳ `email` | string | User email |
| ↳ `isBookmarked` | boolean | Whether the issue is bookmarked |
| ↳ `isSubscribed` | boolean | Whether the user is subscribed to updates |
| ↳ `hasSeen` | boolean | Whether the user has seen this issue |
@@ -194,7 +184,7 @@ Update a Sentry issue by changing its status, assignment, bookmark state, or oth
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `issue` | object | The updated Sentry issue |
| ↳ `id` | string | User ID |
| ↳ `id` | string | Unique issue ID |
| ↳ `shortId` | string | Short issue identifier |
| ↳ `title` | string | Issue title |
| ↳ `status` | string | Updated issue status |
@@ -202,8 +192,6 @@ Update a Sentry issue by changing its status, assignment, bookmark state, or oth
| ↳ `id` | string | User ID |
| ↳ `name` | string | User name |
| ↳ `email` | string | User email |
| ↳ `name` | string | User name |
| ↳ `email` | string | User email |
| ↳ `isBookmarked` | boolean | Whether the issue is bookmarked |
| ↳ `isSubscribed` | boolean | Whether the user is subscribed to updates |
| ↳ `isPublic` | boolean | Whether the issue is publicly visible |
@@ -227,9 +215,9 @@ List all projects in a Sentry organization. Returns project details including na
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `projects` | array | List of Sentry projects |
| ↳ `id` | string | Team ID |
| ↳ `slug` | string | Team slug |
| ↳ `name` | string | Team name |
| ↳ `id` | string | Unique project ID |
| ↳ `slug` | string | URL-friendly project identifier |
| ↳ `name` | string | Project name |
| ↳ `platform` | string | Platform/language \(e.g., javascript, python\) |
| ↳ `dateCreated` | string | When the project was created \(ISO timestamp\) |
| ↳ `isBookmarked` | boolean | Whether the project is bookmarked |
@@ -266,9 +254,9 @@ Retrieve detailed information about a specific Sentry project by its slug. Retur
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `project` | object | Detailed information about the Sentry project |
| ↳ `id` | string | Team ID |
| ↳ `slug` | string | Team slug |
| ↳ `name` | string | Team name |
| ↳ `id` | string | Unique project ID |
| ↳ `slug` | string | URL-friendly project identifier |
| ↳ `name` | string | Project name |
| ↳ `platform` | string | Platform/language \(e.g., javascript, python\) |
| ↳ `dateCreated` | string | When the project was created \(ISO timestamp\) |
| ↳ `isBookmarked` | boolean | Whether the project is bookmarked |
@@ -321,9 +309,9 @@ Create a new Sentry project in an organization. Requires a team to associate the
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `project` | object | The newly created Sentry project |
| ↳ `id` | string | Team ID |
| ↳ `slug` | string | Team slug |
| ↳ `name` | string | Team name |
| ↳ `id` | string | Unique project ID |
| ↳ `slug` | string | URL-friendly project identifier |
| ↳ `name` | string | Project name |
| ↳ `platform` | string | Platform/language |
| ↳ `dateCreated` | string | When the project was created \(ISO timestamp\) |
| ↳ `isBookmarked` | boolean | Whether the project is bookmarked |
@@ -370,9 +358,9 @@ Update a Sentry project by changing its name, slug, platform, or other settings.
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `project` | object | The updated Sentry project |
| ↳ `id` | string | Team ID |
| ↳ `slug` | string | Team slug |
| ↳ `name` | string | Team name |
| ↳ `id` | string | Unique project ID |
| ↳ `slug` | string | URL-friendly project identifier |
| ↳ `name` | string | Project name |
| ↳ `platform` | string | Platform/language |
| ↳ `isBookmarked` | boolean | Whether the project is bookmarked |
| ↳ `organization` | object | Organization information |
@@ -406,7 +394,7 @@ List events from a Sentry project. Can be filtered by issue ID, query, or time p
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `events` | array | List of Sentry events |
| ↳ `id` | string | User ID |
| ↳ `id` | string | Unique event ID |
| ↳ `eventID` | string | Event identifier |
| ↳ `projectID` | string | Project ID |
| ↳ `groupID` | string | Issue group ID |
@@ -422,23 +410,16 @@ List events from a Sentry project. Can be filtered by issue ID, query, or time p
| ↳ `username` | string | Username |
| ↳ `ipAddress` | string | IP address |
| ↳ `name` | string | User display name |
| ↳ `email` | string | User email |
| ↳ `username` | string | Username |
| ↳ `ipAddress` | string | IP address |
| ↳ `name` | string | SDK name |
| ↳ `tags` | array | Tags associated with the event |
| ↳ `key` | string | Tag key |
| ↳ `value` | string | Tag value |
| ↳ `key` | string | Tag key |
| ↳ `value` | string | Error message or value |
| ↳ `contexts` | object | Additional context data \(device, OS, etc.\) |
| ↳ `platform` | string | Platform where the event occurred |
| ↳ `type` | string | Type of error \(e.g., TypeError\) |
| ↳ `type` | string | Event type |
| ↳ `metadata` | object | Error metadata |
| ↳ `type` | string | Type of error \(e.g., TypeError\) |
| ↳ `value` | string | Error message or value |
| ↳ `function` | string | Function where the error occurred |
| ↳ `function` | string | Function where the error occurred |
| ↳ `entries` | array | Event entries \(exception, breadcrumbs, etc.\) |
| ↳ `errors` | array | Processing errors |
| ↳ `dist` | string | Distribution identifier |
@@ -446,7 +427,6 @@ List events from a Sentry project. Can be filtered by issue ID, query, or time p
| ↳ `sdk` | object | SDK information |
| ↳ `name` | string | SDK name |
| ↳ `version` | string | SDK version |
| ↳ `version` | string | SDK version |
| `metadata` | object | Pagination metadata |
| ↳ `nextCursor` | string | Cursor for the next page of results \(if available\) |
| ↳ `hasMore` | boolean | Whether there are more results available |
@@ -469,7 +449,7 @@ Retrieve detailed information about a specific Sentry event by its ID. Returns c
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `event` | object | Detailed information about the Sentry event |
| ↳ `id` | string | User ID |
| ↳ `id` | string | Unique event ID |
| ↳ `eventID` | string | Event identifier |
| ↳ `projectID` | string | Project ID |
| ↳ `groupID` | string | Issue group ID this event belongs to |
@@ -485,23 +465,16 @@ Retrieve detailed information about a specific Sentry event by its ID. Returns c
| ↳ `username` | string | Username |
| ↳ `ipAddress` | string | IP address |
| ↳ `name` | string | User display name |
| ↳ `email` | string | User email |
| ↳ `username` | string | Username |
| ↳ `ipAddress` | string | IP address |
| ↳ `name` | string | SDK name |
| ↳ `tags` | array | Tags associated with the event |
| ↳ `key` | string | Tag key |
| ↳ `value` | string | Tag value |
| ↳ `key` | string | Tag key |
| ↳ `value` | string | Error message or value |
| ↳ `contexts` | object | Additional context data \(device, OS, browser, etc.\) |
| ↳ `platform` | string | Platform where the event occurred |
| ↳ `type` | string | Type of error \(e.g., TypeError, ValueError\) |
| ↳ `type` | string | Event type \(error, transaction, etc.\) |
| ↳ `metadata` | object | Error metadata |
| ↳ `type` | string | Type of error \(e.g., TypeError, ValueError\) |
| ↳ `value` | string | Error message or value |
| ↳ `function` | string | Function where the error occurred |
| ↳ `function` | string | Function where the error occurred |
| ↳ `entries` | array | Event entries including exception, breadcrumbs, and request data |
| ↳ `errors` | array | Processing errors that occurred |
| ↳ `dist` | string | Distribution identifier |
@@ -509,7 +482,6 @@ Retrieve detailed information about a specific Sentry event by its ID. Returns c
| ↳ `sdk` | object | SDK information |
| ↳ `name` | string | SDK name |
| ↳ `version` | string | SDK version |
| ↳ `version` | string | SDK version |
### `sentry_releases_list`
@@ -531,36 +503,30 @@ List releases for a Sentry organization or project. Returns release details incl
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `releases` | array | List of Sentry releases |
| ↳ `id` | string | Project ID |
| ↳ `version` | object | Version details |
| ↳ `raw` | string | Raw version string |
| ↳ `id` | string | Unique release ID |
| ↳ `version` | string | Release version identifier |
| ↳ `shortVersion` | string | Shortened version identifier |
| ↳ `ref` | string | Git reference \(commit SHA, tag, or branch\) |
| ↳ `url` | string | URL to the release \(e.g., GitHub release page\) |
| ↳ `dateReleased` | string | When the release was deployed \(ISO timestamp\) |
| ↳ `dateCreated` | string | Commit timestamp |
| ↳ `dateStarted` | string | Deploy start timestamp |
| ↳ `dateCreated` | string | When the release was created \(ISO timestamp\) |
| ↳ `dateStarted` | string | When the release started \(ISO timestamp\) |
| ↳ `newGroups` | number | Number of new issues introduced in this release |
| ↳ `owner` | object | Owner of the release |
| ↳ `id` | string | User ID |
| ↳ `name` | string | User name |
| ↳ `email` | string | User email |
| ↳ `name` | string | Project name |
| ↳ `email` | string | Author email |
| ↳ `commitCount` | number | Number of commits in this release |
| ↳ `deployCount` | number | Number of deploys for this release |
| ↳ `lastCommit` | object | Last commit in the release |
| ↳ `id` | string | Commit SHA |
| ↳ `message` | string | Commit message |
| ↳ `dateCreated` | string | Commit timestamp |
| ↳ `message` | string | Commit message |
| ↳ `lastDeploy` | object | Last deploy of the release |
| ↳ `id` | string | Deploy ID |
| ↳ `environment` | string | Deploy environment |
| ↳ `dateStarted` | string | Deploy start timestamp |
| ↳ `dateFinished` | string | Deploy finish timestamp |
| ↳ `environment` | string | Deploy environment |
| ↳ `dateFinished` | string | Deploy finish timestamp |
| ↳ `authors` | array | Authors of commits in the release |
| ↳ `id` | string | Author ID |
| ↳ `name` | string | Author name |
@@ -570,18 +536,12 @@ List releases for a Sentry organization or project. Returns release details incl
| ↳ `name` | string | Project name |
| ↳ `slug` | string | Project slug |
| ↳ `platform` | string | Project platform |
| ↳ `slug` | string | Project slug |
| ↳ `platform` | string | Project platform |
| ↳ `firstEvent` | string | First event timestamp |
| ↳ `lastEvent` | string | Last event timestamp |
| ↳ `versionInfo` | object | Version metadata |
| ↳ `buildHash` | string | Build hash |
| ↳ `version` | object | Version details |
| ↳ `raw` | string | Raw version string |
| ↳ `raw` | string | Raw version string |
| ↳ `package` | string | Package name |
| ↳ `buildHash` | string | Build hash |
| ↳ `raw` | string | Raw version string |
| ↳ `package` | string | Package name |
| `metadata` | object | Pagination metadata |
| ↳ `nextCursor` | string | Cursor for the next page of results \(if available\) |
@@ -609,15 +569,14 @@ Create a new release in Sentry. A release is a version of your code deployed to
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `release` | object | The newly created Sentry release |
| ↳ `id` | string | Project ID |
| ↳ `version` | object | Version details |
| ↳ `raw` | string | Raw version string |
| ↳ `id` | string | Unique release ID |
| ↳ `version` | string | Release version identifier |
| ↳ `shortVersion` | string | Shortened version identifier |
| ↳ `ref` | string | Git reference \(commit SHA, tag, or branch\) |
| ↳ `url` | string | URL to the release |
| ↳ `dateReleased` | string | When the release was deployed \(ISO timestamp\) |
| ↳ `dateCreated` | string | Commit timestamp |
| ↳ `dateStarted` | string | Deploy start timestamp |
| ↳ `dateCreated` | string | When the release was created \(ISO timestamp\) |
| ↳ `dateStarted` | string | When the release started \(ISO timestamp\) |
| ↳ `newGroups` | number | Number of new issues introduced |
| ↳ `commitCount` | number | Number of commits in this release |
| ↳ `deployCount` | number | Number of deploys for this release |
@@ -625,20 +584,15 @@ Create a new release in Sentry. A release is a version of your code deployed to
| ↳ `id` | string | Owner ID |
| ↳ `name` | string | Owner name |
| ↳ `email` | string | Owner email |
| ↳ `name` | string | Project name |
| ↳ `email` | string | Author email |
| ↳ `lastCommit` | object | Last commit in the release |
| ↳ `id` | string | Commit SHA |
| ↳ `message` | string | Commit message |
| ↳ `dateCreated` | string | Commit timestamp |
| ↳ `message` | string | Commit message |
| ↳ `lastDeploy` | object | Last deploy of the release |
| ↳ `id` | string | Deploy ID |
| ↳ `environment` | string | Deploy environment |
| ↳ `dateStarted` | string | Deploy start timestamp |
| ↳ `dateFinished` | string | Deploy finish timestamp |
| ↳ `environment` | string | Deploy environment |
| ↳ `dateFinished` | string | Deploy finish timestamp |
| ↳ `authors` | array | Authors of commits in the release |
| ↳ `id` | string | Author ID |
| ↳ `name` | string | Author name |
@@ -648,19 +602,13 @@ Create a new release in Sentry. A release is a version of your code deployed to
| ↳ `name` | string | Project name |
| ↳ `slug` | string | Project slug |
| ↳ `platform` | string | Project platform |
| ↳ `slug` | string | Project slug |
| ↳ `platform` | string | Project platform |
| ↳ `firstEvent` | string | First event timestamp |
| ↳ `lastEvent` | string | Last event timestamp |
| ↳ `versionInfo` | object | Version metadata |
| ↳ `buildHash` | string | Build hash |
| ↳ `version` | object | Version details |
| ↳ `raw` | string | Raw version string |
| ↳ `raw` | string | Raw version string |
| ↳ `package` | string | Package name |
| ↳ `buildHash` | string | Build hash |
| ↳ `raw` | string | Raw version string |
| ↳ `package` | string | Package name |
### `sentry_releases_deploy`

View File

@@ -95,13 +95,7 @@ Read a specific page from a SharePoint site
| ↳ `pageLayout` | string | The layout type of the page |
| ↳ `createdDateTime` | string | When the page was created |
| ↳ `lastModifiedDateTime` | string | When the page was last modified |
| ↳ `id` | string | The unique ID of the page |
| ↳ `name` | string | The name of the page |
| ↳ `title` | string | The title of the page |
| ↳ `webUrl` | string | The URL to access the page |
| ↳ `pageLayout` | string | The layout type of the page |
| ↳ `createdDateTime` | string | When the page was created |
| ↳ `lastModifiedDateTime` | string | When the page was last modified |
| ↳ `content` | object | Extracted text content from the page |
| ↳ `content` | string | Extracted text content from the page |
| ↳ `canvasLayout` | object | Raw SharePoint canvas layout structure |
| `content` | object | Content of the SharePoint page |
@@ -135,10 +129,8 @@ List details of all SharePoint sites
| ↳ `isPersonalSite` | boolean | Whether this is a personal site |
| ↳ `root` | object | Server relative URL |
| ↳ `serverRelativeUrl` | string | Server relative URL |
| ↳ `serverRelativeUrl` | string | Server relative URL |
| ↳ `siteCollection` | object | Site collection hostname |
| ↳ `hostname` | string | Site collection hostname |
| ↳ `hostname` | string | Site collection hostname |
| `sites` | array | List of all accessible SharePoint sites |
| ↳ `id` | string | The unique ID of the site |
| ↳ `name` | string | The name of the site |
@@ -193,7 +185,7 @@ Get metadata (and optionally columns/items) for a SharePoint list
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `list` | object | Information about the SharePoint list |
| ↳ `id` | string | Item ID |
| ↳ `id` | string | The unique ID of the list |
| ↳ `displayName` | string | The display name of the list |
| ↳ `name` | string | The internal name of the list |
| ↳ `webUrl` | string | The web URL of the list |
@@ -201,7 +193,6 @@ Get metadata (and optionally columns/items) for a SharePoint list
| ↳ `lastModifiedDateTime` | string | When the list was last modified |
| ↳ `list` | object | List properties \(e.g., template\) |
| ↳ `columns` | array | List column definitions |
| ↳ `fields` | object | Field values for the item |
| `lists` | array | All lists in the site when no listId/title provided |
### `sharepoint_update_list`

View File

@@ -147,9 +147,9 @@ Read the latest messages from Slack channels. Retrieve conversation history with
| --------- | ---- | ----------- |
| `messages` | array | Array of message objects from the channel |
| ↳ `type` | string | Message type |
| ↳ `ts` | string | Edit timestamp |
| ↳ `ts` | string | Message timestamp |
| ↳ `text` | string | Message text content |
| ↳ `user` | string | User ID who edited |
| ↳ `user` | string | User ID who sent the message |
| ↳ `bot_id` | string | Bot ID if sent by a bot |
| ↳ `username` | string | Display username |
| ↳ `channel` | string | Channel ID |
@@ -167,9 +167,6 @@ Read the latest messages from Slack channels. Retrieve conversation history with
| ↳ `name` | string | Emoji name |
| ↳ `count` | number | Number of reactions |
| ↳ `users` | array | Array of user IDs who reacted |
| ↳ `name` | string | File name |
| ↳ `count` | number | Number of reactions |
| ↳ `users` | array | Array of user IDs who reacted |
| ↳ `is_starred` | boolean | Whether message is starred |
| ↳ `pinned_to` | array | Array of channel IDs where message is pinned |
| ↳ `files` | array | Array of files attached to message |
@@ -180,17 +177,12 @@ Read the latest messages from Slack channels. Retrieve conversation history with
| ↳ `url_private` | string | Private download URL |
| ↳ `permalink` | string | Permanent link to file |
| ↳ `mode` | string | File mode |
| ↳ `id` | string | File ID |
| ↳ `mimetype` | string | MIME type |
| ↳ `size` | number | File size in bytes |
| ↳ `url_private` | string | Private download URL |
| ↳ `permalink` | string | Permanent link to message |
| ↳ `mode` | string | File mode |
| ↳ `attachments` | array | Array of legacy attachments |
| ↳ `blocks` | array | Array of Block Kit blocks |
| ↳ `edited` | object | Edit information if message was edited |
| ↳ `user` | string | User ID who edited |
| ↳ `ts` | string | Edit timestamp |
| ↳ `permalink` | string | Permanent link to message |
### `slack_get_message`
@@ -211,9 +203,9 @@ Retrieve a specific message by its timestamp. Useful for getting a thread parent
| --------- | ---- | ----------- |
| `message` | object | The retrieved message object |
| ↳ `type` | string | Message type |
| ↳ `ts` | string | Edit timestamp |
| ↳ `ts` | string | Message timestamp |
| ↳ `text` | string | Message text content |
| ↳ `user` | string | User ID who edited |
| ↳ `user` | string | User ID who sent the message |
| ↳ `bot_id` | string | Bot ID if sent by a bot |
| ↳ `username` | string | Display username |
| ↳ `channel` | string | Channel ID |
@@ -228,9 +220,6 @@ Retrieve a specific message by its timestamp. Useful for getting a thread parent
| ↳ `name` | string | Emoji name |
| ↳ `count` | number | Number of reactions |
| ↳ `users` | array | User IDs who reacted |
| ↳ `name` | string | File name |
| ↳ `count` | number | Number of reactions |
| ↳ `users` | array | User IDs who reacted |
| ↳ `is_starred` | boolean | Whether message is starred |
| ↳ `pinned_to` | array | Channel IDs where message is pinned |
| ↳ `files` | array | Files attached to message |
@@ -240,16 +229,12 @@ Retrieve a specific message by its timestamp. Useful for getting a thread parent
| ↳ `size` | number | File size in bytes |
| ↳ `url_private` | string | Private download URL |
| ↳ `permalink` | string | Permanent link to file |
| ↳ `id` | string | File ID |
| ↳ `mimetype` | string | MIME type |
| ↳ `size` | number | File size in bytes |
| ↳ `url_private` | string | Private download URL |
| ↳ `permalink` | string | Permanent link to message |
| ↳ `attachments` | array | Legacy attachments |
| ↳ `blocks` | array | Block Kit blocks |
| ↳ `edited` | object | Edit information if message was edited |
| ↳ `user` | string | User ID who edited |
| ↳ `ts` | string | Edit timestamp |
| ↳ `permalink` | string | Permanent link to message |
### `slack_get_thread`
@@ -283,17 +268,11 @@ Retrieve an entire thread including the parent message and all replies. Useful f
| ↳ `name` | string | Emoji name |
| ↳ `count` | number | Number of reactions |
| ↳ `users` | array | User IDs who reacted |
| ↳ `name` | string | File name |
| ↳ `count` | number | Number of reactions |
| ↳ `users` | array | User IDs who reacted |
| ↳ `files` | array | Files attached to the parent message |
| ↳ `id` | string | File ID |
| ↳ `name` | string | File name |
| ↳ `mimetype` | string | MIME type |
| ↳ `size` | number | File size in bytes |
| ↳ `id` | string | File ID |
| ↳ `mimetype` | string | MIME type |
| ↳ `size` | number | File size in bytes |
| `replies` | array | Array of reply messages in the thread \(excluding the parent\) |
| ↳ `ts` | string | Message timestamp |
| ↳ `text` | string | Message text content |

View File

@@ -86,9 +86,6 @@ Run an autonomous web agent to complete tasks and extract structured data
| ↳ `type` | string | Type of action performed |
| ↳ `params` | object | Parameters used for the action |
| ↳ `result` | object | Result of the action |
| ↳ `type` | string | Type of action performed |
| ↳ `params` | object | Parameters used for the action |
| ↳ `result` | object | Result of the action |
| `structuredOutput` | object | Extracted data matching the provided output schema |

View File

@@ -82,16 +82,11 @@ Send messages to Telegram channels or users through the Telegram Bot API. Enable
| ↳ `is_bot` | boolean | Whether the chat is a bot or not |
| ↳ `first_name` | string | Chat username \(if available\) |
| ↳ `username` | string | Chat title \(for groups and channels\) |
| ↳ `id` | number | Bot user ID |
| ↳ `is_bot` | boolean | Whether the chat is a bot or not |
| ↳ `first_name` | string | Bot first name |
| ↳ `username` | string | Bot username |
| ↳ `chat` | object | Information about the bot that sent the message |
| ↳ `id` | number | Bot user ID |
| ↳ `first_name` | string | Bot first name |
| ↳ `username` | string | Bot username |
| ↳ `type` | string | chat type private or channel |
| ↳ `type` | string | chat type private or channel |
| ↳ `date` | number | Unix timestamp when message was sent |
| ↳ `text` | string | Text content of the sent message |
@@ -141,16 +136,11 @@ Send photos to Telegram channels or users through the Telegram Bot API.
| ↳ `is_bot` | boolean | Whether the chat is a bot or not |
| ↳ `first_name` | string | Chat username \(if available\) |
| ↳ `username` | string | Chat title \(for groups and channels\) |
| ↳ `id` | number | Bot user ID |
| ↳ `is_bot` | boolean | Whether the chat is a bot or not |
| ↳ `first_name` | string | Bot first name |
| ↳ `username` | string | Bot username |
| ↳ `chat` | object | Information about the bot that sent the message |
| ↳ `id` | number | Bot user ID |
| ↳ `first_name` | string | Bot first name |
| ↳ `username` | string | Bot username |
| ↳ `type` | string | Chat type \(private, group, supergroup, channel\) |
| ↳ `type` | string | Chat type \(private, group, supergroup, channel\) |
| ↳ `date` | number | Unix timestamp when message was sent |
| ↳ `text` | string | Text content of the sent message \(if applicable\) |
| ↳ `photo` | array | List of photos included in the message |
@@ -159,11 +149,6 @@ Send photos to Telegram channels or users through the Telegram Bot API.
| ↳ `file_size` | number | Size of the photo file in bytes |
| ↳ `width` | number | Photo width in pixels |
| ↳ `height` | number | Photo height in pixels |
| ↳ `file_id` | string | Unique file ID of the photo |
| ↳ `file_unique_id` | string | Unique identifier for this file across different bots |
| ↳ `file_size` | number | Size of the photo file in bytes |
| ↳ `width` | number | Photo width in pixels |
| ↳ `height` | number | Photo height in pixels |
### `telegram_send_video`
@@ -190,25 +175,26 @@ Send videos to Telegram channels or users through the Telegram Bot API.
| ↳ `is_bot` | boolean | Whether the chat is a bot or not |
| ↳ `first_name` | string | Sender |
| ↳ `username` | string | Sender |
| ↳ `id` | number | Chat ID |
| ↳ `is_bot` | boolean | Whether the chat is a bot or not |
| ↳ `first_name` | string | Chat first name \(if private chat\) |
| ↳ `username` | string | Chat username \(for private or channels\) |
| ↳ `chat` | object | Information about the chat where message was sent |
| ↳ `id` | number | Chat ID |
| ↳ `first_name` | string | Chat first name \(if private chat\) |
| ↳ `username` | string | Chat username \(for private or channels\) |
| ↳ `type` | string | Type of chat \(private, group, supergroup, or channel\) |
| ↳ `type` | string | Type of chat \(private, group, supergroup, or channel\) |
| ↳ `date` | number | Unix timestamp when the message was sent |
| ↳ `text` | string | Text content of the sent message \(if applicable\) |
| ↳ `format` | object | Media format information \(for videos, GIFs, etc.\) |
| ↳ `file_name` | string | Media file name |
| ↳ `mime_type` | string | Media MIME type |
| ↳ `duration` | number | Duration of media in seconds |
| ↳ `width` | number | Media width in pixels |
| ↳ `height` | number | Media height in pixels |
| ↳ `thumbnail` | object | Thumbnail image details |
| ↳ `file_id` | string | Thumbnail file ID |
| ↳ `file_unique_id` | string | Unique thumbnail file identifier |
| ↳ `file_size` | number | Thumbnail file size in bytes |
| ↳ `width` | number | Thumbnail width in pixels |
| ↳ `height` | number | Thumbnail height in pixels |
| ↳ `thumbnail` | object | Thumbnail image details |
| ↳ `thumb` | object | Secondary thumbnail details \(duplicate of thumbnail\) |
| ↳ `file_id` | string | Thumbnail file ID |
| ↳ `file_unique_id` | string | Unique thumbnail file identifier |
| ↳ `file_size` | number | Thumbnail file size in bytes |
@@ -217,32 +203,6 @@ Send videos to Telegram channels or users through the Telegram Bot API.
| ↳ `file_id` | string | Media file ID |
| ↳ `file_unique_id` | string | Unique media file identifier |
| ↳ `file_size` | number | Size of media file in bytes |
| ↳ `thumb` | object | Secondary thumbnail details \(duplicate of thumbnail\) |
| ↳ `file_id` | string | Thumbnail file ID |
| ↳ `file_unique_id` | string | Unique thumbnail file identifier |
| ↳ `file_size` | number | Thumbnail file size in bytes |
| ↳ `width` | number | Thumbnail width in pixels |
| ↳ `height` | number | Thumbnail height in pixels |
| ↳ `file_name` | string | Document file name |
| ↳ `mime_type` | string | Document MIME type |
| ↳ `duration` | number | Duration of media in seconds |
| ↳ `width` | number | Thumbnail width in pixels |
| ↳ `height` | number | Thumbnail height in pixels |
| ↳ `thumbnail` | object | Document thumbnail information |
| ↳ `file_id` | string | Thumbnail file ID |
| ↳ `file_unique_id` | string | Unique thumbnail file identifier |
| ↳ `file_size` | number | Thumbnail file size in bytes |
| ↳ `width` | number | Thumbnail width in pixels |
| ↳ `height` | number | Thumbnail height in pixels |
| ↳ `file_id` | string | Document file ID |
| ↳ `file_unique_id` | string | Unique document file identifier |
| ↳ `file_size` | number | Size of document file in bytes |
| ↳ `thumb` | object | Duplicate thumbnail info \(used for compatibility\) |
| ↳ `file_id` | string | Thumbnail file ID |
| ↳ `file_unique_id` | string | Unique thumbnail file identifier |
| ↳ `file_size` | number | Thumbnail file size in bytes |
| ↳ `width` | number | Thumbnail width in pixels |
| ↳ `height` | number | Thumbnail height in pixels |
| ↳ `document` | object | Document file details if the message contains a document |
| ↳ `file_name` | string | Document file name |
| ↳ `mime_type` | string | Document MIME type |
@@ -252,17 +212,15 @@ Send videos to Telegram channels or users through the Telegram Bot API.
| ↳ `file_size` | number | Thumbnail file size in bytes |
| ↳ `width` | number | Thumbnail width in pixels |
| ↳ `height` | number | Thumbnail height in pixels |
| ↳ `file_id` | string | Document file ID |
| ↳ `file_unique_id` | string | Unique document file identifier |
| ↳ `file_size` | number | Size of document file in bytes |
| ↳ `width` | number | Thumbnail width in pixels |
| ↳ `height` | number | Thumbnail height in pixels |
| ↳ `thumb` | object | Duplicate thumbnail info \(used for compatibility\) |
| ↳ `file_id` | string | Thumbnail file ID |
| ↳ `file_unique_id` | string | Unique thumbnail file identifier |
| ↳ `file_size` | number | Thumbnail file size in bytes |
| ↳ `width` | number | Thumbnail width in pixels |
| ↳ `height` | number | Thumbnail height in pixels |
| ↳ `file_id` | string | Document file ID |
| ↳ `file_unique_id` | string | Unique document file identifier |
| ↳ `file_size` | number | Size of document file in bytes |
### `telegram_send_audio`
@@ -289,16 +247,11 @@ Send audio files to Telegram channels or users through the Telegram Bot API.
| ↳ `is_bot` | boolean | Whether the chat is a bot or not |
| ↳ `first_name` | string | Sender |
| ↳ `username` | string | Sender |
| ↳ `id` | number | Chat ID |
| ↳ `is_bot` | boolean | Whether the chat is a bot or not |
| ↳ `first_name` | string | Chat first name \(if private chat\) |
| ↳ `username` | string | Chat username \(for private or channels\) |
| ↳ `chat` | object | Information about the chat where the message was sent |
| ↳ `id` | number | Chat ID |
| ↳ `first_name` | string | Chat first name \(if private chat\) |
| ↳ `username` | string | Chat username \(for private or channels\) |
| ↳ `type` | string | Type of chat \(private, group, supergroup, or channel\) |
| ↳ `type` | string | Type of chat \(private, group, supergroup, or channel\) |
| ↳ `date` | number | Unix timestamp when the message was sent |
| ↳ `text` | string | Text content of the sent message \(if applicable\) |
| ↳ `audio` | object | Audio file details |
@@ -310,14 +263,6 @@ Send audio files to Telegram channels or users through the Telegram Bot API.
| ↳ `file_id` | string | Unique file identifier for this audio |
| ↳ `file_unique_id` | string | Unique identifier across different bots for this file |
| ↳ `file_size` | number | Size of the audio file in bytes |
| ↳ `duration` | number | Duration of the audio in seconds |
| ↳ `performer` | string | Performer of the audio |
| ↳ `title` | string | Title of the audio |
| ↳ `file_name` | string | Original filename of the audio |
| ↳ `mime_type` | string | MIME type of the audio file |
| ↳ `file_id` | string | Unique file identifier for this audio |
| ↳ `file_unique_id` | string | Unique identifier across different bots for this file |
| ↳ `file_size` | number | Size of the audio file in bytes |
### `telegram_send_animation`
@@ -344,25 +289,26 @@ Send animations (GIFs) to Telegram channels or users through the Telegram Bot AP
| ↳ `is_bot` | boolean | Whether the chat is a bot or not |
| ↳ `first_name` | string | Sender |
| ↳ `username` | string | Sender |
| ↳ `id` | number | Chat ID |
| ↳ `is_bot` | boolean | Whether the chat is a bot or not |
| ↳ `first_name` | string | Chat first name \(if private chat\) |
| ↳ `username` | string | Chat username \(for private or channels\) |
| ↳ `chat` | object | Information about the chat where message was sent |
| ↳ `id` | number | Chat ID |
| ↳ `first_name` | string | Chat first name \(if private chat\) |
| ↳ `username` | string | Chat username \(for private or channels\) |
| ↳ `type` | string | Type of chat \(private, group, supergroup, or channel\) |
| ↳ `type` | string | Type of chat \(private, group, supergroup, or channel\) |
| ↳ `date` | number | Unix timestamp when the message was sent |
| ↳ `text` | string | Text content of the sent message \(if applicable\) |
| ↳ `format` | object | Media format information \(for videos, GIFs, etc.\) |
| ↳ `file_name` | string | Media file name |
| ↳ `mime_type` | string | Media MIME type |
| ↳ `duration` | number | Duration of media in seconds |
| ↳ `width` | number | Media width in pixels |
| ↳ `height` | number | Media height in pixels |
| ↳ `thumbnail` | object | Thumbnail image details |
| ↳ `file_id` | string | Thumbnail file ID |
| ↳ `file_unique_id` | string | Unique thumbnail file identifier |
| ↳ `file_size` | number | Thumbnail file size in bytes |
| ↳ `width` | number | Thumbnail width in pixels |
| ↳ `height` | number | Thumbnail height in pixels |
| ↳ `thumbnail` | object | Thumbnail image details |
| ↳ `thumb` | object | Secondary thumbnail details \(duplicate of thumbnail\) |
| ↳ `file_id` | string | Thumbnail file ID |
| ↳ `file_unique_id` | string | Unique thumbnail file identifier |
| ↳ `file_size` | number | Thumbnail file size in bytes |
@@ -371,32 +317,6 @@ Send animations (GIFs) to Telegram channels or users through the Telegram Bot AP
| ↳ `file_id` | string | Media file ID |
| ↳ `file_unique_id` | string | Unique media file identifier |
| ↳ `file_size` | number | Size of media file in bytes |
| ↳ `thumb` | object | Secondary thumbnail details \(duplicate of thumbnail\) |
| ↳ `file_id` | string | Thumbnail file ID |
| ↳ `file_unique_id` | string | Unique thumbnail file identifier |
| ↳ `file_size` | number | Thumbnail file size in bytes |
| ↳ `width` | number | Thumbnail width in pixels |
| ↳ `height` | number | Thumbnail height in pixels |
| ↳ `file_name` | string | Document file name |
| ↳ `mime_type` | string | Document MIME type |
| ↳ `duration` | number | Duration of media in seconds |
| ↳ `width` | number | Thumbnail width in pixels |
| ↳ `height` | number | Thumbnail height in pixels |
| ↳ `thumbnail` | object | Document thumbnail information |
| ↳ `file_id` | string | Thumbnail file ID |
| ↳ `file_unique_id` | string | Unique thumbnail file identifier |
| ↳ `file_size` | number | Thumbnail file size in bytes |
| ↳ `width` | number | Thumbnail width in pixels |
| ↳ `height` | number | Thumbnail height in pixels |
| ↳ `file_id` | string | Document file ID |
| ↳ `file_unique_id` | string | Unique document file identifier |
| ↳ `file_size` | number | Size of document file in bytes |
| ↳ `thumb` | object | Duplicate thumbnail info \(used for compatibility\) |
| ↳ `file_id` | string | Thumbnail file ID |
| ↳ `file_unique_id` | string | Unique thumbnail file identifier |
| ↳ `file_size` | number | Thumbnail file size in bytes |
| ↳ `width` | number | Thumbnail width in pixels |
| ↳ `height` | number | Thumbnail height in pixels |
| ↳ `document` | object | Document file details if the message contains a document |
| ↳ `file_name` | string | Document file name |
| ↳ `mime_type` | string | Document MIME type |
@@ -406,17 +326,15 @@ Send animations (GIFs) to Telegram channels or users through the Telegram Bot AP
| ↳ `file_size` | number | Thumbnail file size in bytes |
| ↳ `width` | number | Thumbnail width in pixels |
| ↳ `height` | number | Thumbnail height in pixels |
| ↳ `file_id` | string | Document file ID |
| ↳ `file_unique_id` | string | Unique document file identifier |
| ↳ `file_size` | number | Size of document file in bytes |
| ↳ `width` | number | Thumbnail width in pixels |
| ↳ `height` | number | Thumbnail height in pixels |
| ↳ `thumb` | object | Duplicate thumbnail info \(used for compatibility\) |
| ↳ `file_id` | string | Thumbnail file ID |
| ↳ `file_unique_id` | string | Unique thumbnail file identifier |
| ↳ `file_size` | number | Thumbnail file size in bytes |
| ↳ `width` | number | Thumbnail width in pixels |
| ↳ `height` | number | Thumbnail height in pixels |
| ↳ `file_id` | string | Document file ID |
| ↳ `file_unique_id` | string | Unique document file identifier |
| ↳ `file_size` | number | Size of document file in bytes |
### `telegram_send_document`
@@ -443,16 +361,11 @@ Send documents (PDF, ZIP, DOC, etc.) to Telegram channels or users through the T
| ↳ `is_bot` | boolean | Whether the chat is a bot or not |
| ↳ `first_name` | string | Sender |
| ↳ `username` | string | Sender |
| ↳ `id` | number | Chat ID |
| ↳ `is_bot` | boolean | Whether the chat is a bot or not |
| ↳ `first_name` | string | Chat first name \(if private chat\) |
| ↳ `username` | string | Chat username \(for private or channels\) |
| ↳ `chat` | object | Information about the chat where message was sent |
| ↳ `id` | number | Chat ID |
| ↳ `first_name` | string | Chat first name \(if private chat\) |
| ↳ `username` | string | Chat username \(for private or channels\) |
| ↳ `type` | string | Type of chat \(private, group, supergroup, or channel\) |
| ↳ `type` | string | Type of chat \(private, group, supergroup, or channel\) |
| ↳ `date` | number | Unix timestamp when the message was sent |
| ↳ `document` | object | Document file details |
| ↳ `file_name` | string | Document file name |
@@ -460,10 +373,5 @@ Send documents (PDF, ZIP, DOC, etc.) to Telegram channels or users through the T
| ↳ `file_id` | string | Document file ID |
| ↳ `file_unique_id` | string | Unique document file identifier |
| ↳ `file_size` | number | Size of document file in bytes |
| ↳ `file_name` | string | Document file name |
| ↳ `mime_type` | string | Document MIME type |
| ↳ `file_id` | string | Document file ID |
| ↳ `file_unique_id` | string | Unique document file identifier |
| ↳ `file_size` | number | Size of document file in bytes |

View File

@@ -63,7 +63,7 @@ Parse documents using AWS Textract OCR and document analysis
| `blocks` | array | Array of Block objects containing detected text, tables, forms, and other elements |
| ↳ `BlockType` | string | Type of block \(PAGE, LINE, WORD, TABLE, CELL, KEY_VALUE_SET, etc.\) |
| ↳ `Id` | string | Unique identifier for the block |
| ↳ `Text` | string | Query text |
| ↳ `Text` | string | The text content \(for LINE and WORD blocks\) |
| ↳ `TextType` | string | Type of text \(PRINTED or HANDWRITING\) |
| ↳ `Confidence` | number | Confidence score \(0-100\) |
| ↳ `Page` | number | Page number |
@@ -73,34 +73,12 @@ Parse documents using AWS Textract OCR and document analysis
| ↳ `Left` | number | Left position as ratio of document width |
| ↳ `Top` | number | Top position as ratio of document height |
| ↳ `Width` | number | Width as ratio of document width |
| ↳ `Height` | number | Height as ratio of document height |
| ↳ `Left` | number | Left position as ratio of document width |
| ↳ `Top` | number | Top position as ratio of document height |
| ↳ `Width` | number | Width as ratio of document width |
| ↳ `Polygon` | array | Polygon coordinates |
| ↳ `X` | number | X coordinate |
| ↳ `Y` | number | Y coordinate |
| ↳ `X` | number | X coordinate |
| ↳ `Y` | number | Y coordinate |
| ↳ `BoundingBox` | object | Height as ratio of document height |
| ↳ `Height` | number | Height as ratio of document height |
| ↳ `Left` | number | Left position as ratio of document width |
| ↳ `Top` | number | Top position as ratio of document height |
| ↳ `Width` | number | Width as ratio of document width |
| ↳ `Height` | number | Height as ratio of document height |
| ↳ `Left` | number | Left position as ratio of document width |
| ↳ `Top` | number | Top position as ratio of document height |
| ↳ `Width` | number | Width as ratio of document width |
| ↳ `Polygon` | array | Polygon coordinates |
| ↳ `X` | number | X coordinate |
| ↳ `Y` | number | Y coordinate |
| ↳ `X` | number | X coordinate |
| ↳ `Y` | number | Y coordinate |
| ↳ `Relationships` | array | Relationships to other blocks |
| ↳ `Type` | string | Relationship type \(CHILD, VALUE, ANSWER, etc.\) |
| ↳ `Ids` | array | IDs of related blocks |
| ↳ `Type` | string | Relationship type \(CHILD, VALUE, ANSWER, etc.\) |
| ↳ `Ids` | array | IDs of related blocks |
| ↳ `EntityTypes` | array | Entity types for KEY_VALUE_SET \(KEY or VALUE\) |
| ↳ `SelectionStatus` | string | For checkboxes: SELECTED or NOT_SELECTED |
| ↳ `RowIndex` | number | Row index for table cells |
@@ -111,8 +89,6 @@ Parse documents using AWS Textract OCR and document analysis
| ↳ `Text` | string | Query text |
| ↳ `Alias` | string | Query alias |
| ↳ `Pages` | array | Pages to search |
| ↳ `Alias` | string | Query alias |
| ↳ `Pages` | array | Pages to search |
| `documentMetadata` | object | Metadata about the analyzed document |
| ↳ `pages` | number | Number of pages in the document |
| `modelVersion` | string | Version of the Textract model used for processing |

View File

@@ -109,12 +109,6 @@ Retrieve insights and analytics for Typeform forms
| ↳ `responses_count` | number | Number of responses from this platform |
| ↳ `total_visits` | number | Total visits from this platform |
| ↳ `unique_visits` | number | Unique visits from this platform |
| ↳ `average_time` | number | Overall average completion time |
| ↳ `completion_rate` | number | Overall completion rate |
| ↳ `platform` | string | Platform name \(e.g., desktop, mobile\) |
| ↳ `responses_count` | number | Total number of responses |
| ↳ `total_visits` | number | Total number of visits |
| ↳ `unique_visits` | number | Total number of unique visits |
| ↳ `summary` | object | Overall average completion time |
| ↳ `average_time` | number | Overall average completion time |
| ↳ `completion_rate` | number | Overall completion rate |

View File

@@ -56,9 +56,6 @@ Read content from a Wealthbox note
| ↳ `itemId` | string | ID of the note |
| ↳ `noteId` | string | ID of the note |
| ↳ `itemType` | string | Type of item \(note\) |
| ↳ `itemId` | string | ID of the note |
| ↳ `noteId` | string | ID of the note |
| ↳ `itemType` | string | Type of item \(note\) |
### `wealthbox_write_note`
@@ -83,9 +80,6 @@ Create or update a Wealthbox note
| ↳ `itemId` | string | ID of the created/updated note |
| ↳ `noteId` | string | ID of the created/updated note |
| ↳ `itemType` | string | Type of item \(note\) |
| ↳ `itemId` | string | ID of the created/updated note |
| ↳ `noteId` | string | ID of the created/updated note |
| ↳ `itemType` | string | Type of item \(note\) |
### `wealthbox_read_contact`
@@ -109,9 +103,6 @@ Read content from a Wealthbox contact
| ↳ `itemId` | string | ID of the contact |
| ↳ `contactId` | string | ID of the contact |
| ↳ `itemType` | string | Type of item \(contact\) |
| ↳ `itemId` | string | ID of the contact |
| ↳ `contactId` | string | ID of the contact |
| ↳ `itemType` | string | Type of item \(contact\) |
### `wealthbox_write_contact`
@@ -138,9 +129,6 @@ Create a new Wealthbox contact
| ↳ `itemId` | string | ID of the created/updated contact |
| ↳ `contactId` | string | ID of the created/updated contact |
| ↳ `itemType` | string | Type of item \(contact\) |
| ↳ `itemId` | string | ID of the created/updated contact |
| ↳ `contactId` | string | ID of the created/updated contact |
| ↳ `itemType` | string | Type of item \(contact\) |
### `wealthbox_read_task`
@@ -164,9 +152,6 @@ Read content from a Wealthbox task
| ↳ `itemId` | string | ID of the task |
| ↳ `taskId` | string | ID of the task |
| ↳ `itemType` | string | Type of item \(task\) |
| ↳ `itemId` | string | ID of the task |
| ↳ `taskId` | string | ID of the task |
| ↳ `itemType` | string | Type of item \(task\) |
### `wealthbox_write_task`
@@ -193,8 +178,5 @@ Create or update a Wealthbox task
| ↳ `itemId` | string | ID of the created/updated task |
| ↳ `taskId` | string | ID of the created/updated task |
| ↳ `itemType` | string | Type of item \(task\) |
| ↳ `itemId` | string | ID of the created/updated task |
| ↳ `taskId` | string | ID of the created/updated task |
| ↳ `itemType` | string | Type of item \(task\) |

View File

@@ -61,8 +61,6 @@ Post new tweets, reply to tweets, or create polls on X (Twitter)
| ↳ `attachments` | object | Media or poll attachments |
| ↳ `mediaKeys` | array | Media attachment keys |
| ↳ `pollId` | string | Poll ID if poll attached |
| ↳ `mediaKeys` | array | Media attachment keys |
| ↳ `pollId` | string | Poll ID if poll attached |
### `x_read`
@@ -139,8 +137,5 @@ Get user profile information
| ↳ `followersCount` | number | Number of followers |
| ↳ `followingCount` | number | Number of users following |
| ↳ `tweetCount` | number | Total number of tweets |
| ↳ `followersCount` | number | Number of followers |
| ↳ `followingCount` | number | Number of users following |
| ↳ `tweetCount` | number | Total number of tweets |

View File

@@ -0,0 +1,216 @@
import { db, workflow as workflowTable } from '@sim/db'
import { createLogger } from '@sim/logger'
import { eq } from 'drizzle-orm'
import { type NextRequest, NextResponse } from 'next/server'
import { v4 as uuidv4 } from 'uuid'
import { z } from 'zod'
import { checkHybridAuth } from '@/lib/auth/hybrid'
import { generateRequestId } from '@/lib/core/utils/request'
import { SSE_HEADERS } from '@/lib/core/utils/sse'
import { markExecutionCancelled } from '@/lib/execution/cancellation'
import { LoggingSession } from '@/lib/logs/execution/logging-session'
import { executeWorkflowCore } from '@/lib/workflows/executor/execution-core'
import { createSSECallbacks } from '@/lib/workflows/executor/execution-events'
import { ExecutionSnapshot } from '@/executor/execution/snapshot'
import type { ExecutionMetadata, SerializableExecutionState } from '@/executor/execution/types'
import { hasExecutionResult } from '@/executor/utils/errors'
const logger = createLogger('ExecuteFromBlockAPI')
const ExecuteFromBlockSchema = z.object({
startBlockId: z.string().min(1, 'Start block ID is required'),
sourceSnapshot: z.object({
blockStates: z.record(z.any()),
executedBlocks: z.array(z.string()),
blockLogs: z.array(z.any()),
decisions: z.object({
router: z.record(z.string()),
condition: z.record(z.string()),
}),
completedLoops: z.array(z.string()),
loopExecutions: z.record(z.any()).optional(),
parallelExecutions: z.record(z.any()).optional(),
parallelBlockMapping: z.record(z.any()).optional(),
activeExecutionPath: z.array(z.string()),
}),
input: z.any().optional(),
})
export const runtime = 'nodejs'
export const dynamic = 'force-dynamic'
export async function POST(req: NextRequest, { params }: { params: Promise<{ id: string }> }) {
const requestId = generateRequestId()
const { id: workflowId } = await params
try {
const auth = await checkHybridAuth(req, { requireWorkflowId: false })
if (!auth.success || !auth.userId) {
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
}
const userId = auth.userId
let body: unknown
try {
body = await req.json()
} catch {
return NextResponse.json({ error: 'Invalid JSON body' }, { status: 400 })
}
const validation = ExecuteFromBlockSchema.safeParse(body)
if (!validation.success) {
logger.warn(`[${requestId}] Invalid request body:`, validation.error.errors)
return NextResponse.json(
{
error: 'Invalid request body',
details: validation.error.errors.map((e) => ({
path: e.path.join('.'),
message: e.message,
})),
},
{ status: 400 }
)
}
const { startBlockId, sourceSnapshot, input } = validation.data
const executionId = uuidv4()
const [workflowRecord] = await db
.select({ workspaceId: workflowTable.workspaceId, userId: workflowTable.userId })
.from(workflowTable)
.where(eq(workflowTable.id, workflowId))
.limit(1)
if (!workflowRecord?.workspaceId) {
return NextResponse.json({ error: 'Workflow not found or has no workspace' }, { status: 404 })
}
const workspaceId = workflowRecord.workspaceId
const workflowUserId = workflowRecord.userId
logger.info(`[${requestId}] Starting run-from-block execution`, {
workflowId,
startBlockId,
executedBlocksCount: sourceSnapshot.executedBlocks.length,
})
const loggingSession = new LoggingSession(workflowId, executionId, 'manual', requestId)
const abortController = new AbortController()
let isStreamClosed = false
const stream = new ReadableStream<Uint8Array>({
async start(controller) {
const { sendEvent, onBlockStart, onBlockComplete, onStream } = createSSECallbacks({
executionId,
workflowId,
controller,
isStreamClosed: () => isStreamClosed,
setStreamClosed: () => {
isStreamClosed = true
},
})
const metadata: ExecutionMetadata = {
requestId,
workflowId,
userId,
executionId,
triggerType: 'manual',
workspaceId,
workflowUserId,
useDraftState: true,
isClientSession: true,
startTime: new Date().toISOString(),
}
const snapshot = new ExecutionSnapshot(metadata, {}, input || {}, {})
try {
const startTime = new Date()
sendEvent({
type: 'execution:started',
timestamp: startTime.toISOString(),
executionId,
workflowId,
data: { startTime: startTime.toISOString() },
})
const result = await executeWorkflowCore({
snapshot,
loggingSession,
abortSignal: abortController.signal,
runFromBlock: {
startBlockId,
sourceSnapshot: sourceSnapshot as SerializableExecutionState,
},
callbacks: { onBlockStart, onBlockComplete, onStream },
})
if (result.status === 'cancelled') {
sendEvent({
type: 'execution:cancelled',
timestamp: new Date().toISOString(),
executionId,
workflowId,
data: { duration: result.metadata?.duration || 0 },
})
} else {
sendEvent({
type: 'execution:completed',
timestamp: new Date().toISOString(),
executionId,
workflowId,
data: {
success: result.success,
output: result.output,
duration: result.metadata?.duration || 0,
startTime: result.metadata?.startTime || startTime.toISOString(),
endTime: result.metadata?.endTime || new Date().toISOString(),
},
})
}
} catch (error: unknown) {
const errorMessage = error instanceof Error ? error.message : 'Unknown error'
logger.error(`[${requestId}] Run-from-block execution failed: ${errorMessage}`)
const executionResult = hasExecutionResult(error) ? error.executionResult : undefined
sendEvent({
type: 'execution:error',
timestamp: new Date().toISOString(),
executionId,
workflowId,
data: {
error: executionResult?.error || errorMessage,
duration: executionResult?.metadata?.duration || 0,
},
})
} finally {
if (!isStreamClosed) {
try {
controller.enqueue(new TextEncoder().encode('data: [DONE]\n\n'))
controller.close()
} catch {}
}
}
},
cancel() {
isStreamClosed = true
abortController.abort()
markExecutionCancelled(executionId).catch(() => {})
},
})
return new NextResponse(stream, {
headers: { ...SSE_HEADERS, 'X-Execution-Id': executionId },
})
} catch (error: unknown) {
const errorMessage = error instanceof Error ? error.message : 'Unknown error'
logger.error(`[${requestId}] Failed to start run-from-block execution:`, error)
return NextResponse.json(
{ error: errorMessage || 'Failed to start execution' },
{ status: 500 }
)
}
}

View File

@@ -53,6 +53,7 @@ const ExecuteWorkflowSchema = z.object({
parallels: z.record(z.any()).optional(),
})
.optional(),
stopAfterBlockId: z.string().optional(),
})
export const runtime = 'nodejs'
@@ -222,6 +223,7 @@ export async function POST(req: NextRequest, { params }: { params: Promise<{ id:
includeFileBase64,
base64MaxBytes,
workflowStateOverride,
stopAfterBlockId,
} = validation.data
// For API key and internal JWT auth, the entire body is the input (except for our control fields)
@@ -237,6 +239,7 @@ export async function POST(req: NextRequest, { params }: { params: Promise<{ id:
includeFileBase64,
base64MaxBytes,
workflowStateOverride,
stopAfterBlockId: _stopAfterBlockId,
workflowId: _workflowId, // Also exclude workflowId used for internal JWT auth
...rest
} = body
@@ -434,6 +437,7 @@ export async function POST(req: NextRequest, { params }: { params: Promise<{ id:
loggingSession,
includeFileBase64,
base64MaxBytes,
stopAfterBlockId,
})
const outputWithBase64 = includeFileBase64
@@ -722,6 +726,7 @@ export async function POST(req: NextRequest, { params }: { params: Promise<{ id:
abortSignal: abortController.signal,
includeFileBase64,
base64MaxBytes,
stopAfterBlockId,
})
if (result.status === 'paused') {

View File

@@ -6,7 +6,6 @@ import { useRouter } from 'next/navigation'
import {
Badge,
Button,
Code,
Input,
Label,
Table,
@@ -777,15 +776,6 @@ export default function ResumeExecutionPage({
refreshSelectedDetail,
])
const pauseResponsePreview = useMemo(() => {
if (!selectedDetail?.pausePoint.response?.data) return '{}'
try {
return JSON.stringify(selectedDetail.pausePoint.response.data, null, 2)
} catch {
return String(selectedDetail.pausePoint.response.data)
}
}, [selectedDetail])
const isFormComplete = useMemo(() => {
if (!isHumanMode || !hasInputFormat) return true
return inputFormatFields.every((field) => {
@@ -1155,10 +1145,12 @@ export default function ResumeExecutionPage({
borderBottom: '1px solid var(--border)',
}}
>
<Label>Pause Data</Label>
<Label>Display Data</Label>
</div>
<div style={{ padding: '16px' }}>
<Code.Viewer code={pauseResponsePreview} language='json' />
<p style={{ fontSize: '13px', color: 'var(--text-muted)' }}>
No display data configured
</p>
</div>
</div>
)}

View File

@@ -1,11 +1,13 @@
import { memo, useCallback } from 'react'
import { ArrowLeftRight, ArrowUpDown, Circle, CircleOff, LogOut } from 'lucide-react'
import { ArrowLeftRight, ArrowUpDown, Circle, CircleOff, LogOut, Play } from 'lucide-react'
import { Button, Copy, Tooltip, Trash2 } from '@/components/emcn'
import { cn } from '@/lib/core/utils/cn'
import { isInputDefinitionTrigger } from '@/lib/workflows/triggers/input-definition-triggers'
import { useUserPermissionsContext } from '@/app/workspace/[workspaceId]/providers/workspace-permissions-provider'
import { useWorkflowExecution } from '@/app/workspace/[workspaceId]/w/[workflowId]/hooks'
import { validateTriggerPaste } from '@/app/workspace/[workspaceId]/w/[workflowId]/utils'
import { useCollaborativeWorkflow } from '@/hooks/use-collaborative-workflow'
import { useExecutionStore } from '@/stores/execution'
import { useNotificationStore } from '@/stores/notifications'
import { useWorkflowRegistry } from '@/stores/workflows/registry/store'
import { useWorkflowStore } from '@/stores/workflows/workflow/store'
@@ -49,6 +51,7 @@ export const ActionBar = memo(
collaborativeBatchToggleBlockHandles,
} = useCollaborativeWorkflow()
const { setPendingSelection } = useWorkflowRegistry()
const { handleRunFromBlock } = useWorkflowExecution()
const addNotification = useNotificationStore((s) => s.addNotification)
@@ -97,12 +100,39 @@ export const ActionBar = memo(
)
)
const { activeWorkflowId } = useWorkflowRegistry()
const { isExecuting, getLastExecutionSnapshot } = useExecutionStore()
const userPermissions = useUserPermissionsContext()
const edges = useWorkflowStore((state) => state.edges)
const isStartBlock = isInputDefinitionTrigger(blockType)
const isResponseBlock = blockType === 'response'
const isNoteBlock = blockType === 'note'
const isSubflowBlock = blockType === 'loop' || blockType === 'parallel'
const isInsideSubflow = parentId && (parentType === 'loop' || parentType === 'parallel')
const snapshot = activeWorkflowId ? getLastExecutionSnapshot(activeWorkflowId) : null
const incomingEdges = edges.filter((edge) => edge.target === blockId)
const isTriggerBlock = incomingEdges.length === 0
// Check if each source block is either executed OR is a trigger block (triggers don't need prior execution)
const isSourceSatisfied = (sourceId: string) => {
if (snapshot?.executedBlocks.includes(sourceId)) return true
// Check if source is a trigger (has no incoming edges itself)
const sourceIncomingEdges = edges.filter((edge) => edge.target === sourceId)
return sourceIncomingEdges.length === 0
}
// Non-trigger blocks need a snapshot to exist (so upstream outputs are available)
const dependenciesSatisfied =
isTriggerBlock || (snapshot && incomingEdges.every((edge) => isSourceSatisfied(edge.source)))
const canRunFromBlock =
dependenciesSatisfied && !isNoteBlock && !isInsideSubflow && !isExecuting
const handleRunFromBlockClick = useCallback(() => {
if (!activeWorkflowId || !canRunFromBlock) return
handleRunFromBlock(blockId, activeWorkflowId)
}, [blockId, activeWorkflowId, canRunFromBlock, handleRunFromBlock])
/**
* Get appropriate tooltip message based on disabled state
@@ -128,30 +158,35 @@ export const ActionBar = memo(
'dark:border-transparent dark:bg-[var(--surface-4)]'
)}
>
{!isNoteBlock && (
{!isNoteBlock && !isInsideSubflow && (
<Tooltip.Root>
<Tooltip.Trigger asChild>
<Button
variant='ghost'
onClick={(e) => {
e.stopPropagation()
if (!disabled) {
collaborativeBatchToggleBlockEnabled([blockId])
if (canRunFromBlock && !disabled) {
handleRunFromBlockClick()
}
}}
className={ACTION_BUTTON_STYLES}
disabled={disabled}
disabled={disabled || !canRunFromBlock}
>
{isEnabled ? <Circle className={ICON_SIZE} /> : <CircleOff className={ICON_SIZE} />}
<Play className={ICON_SIZE} />
</Button>
</Tooltip.Trigger>
<Tooltip.Content side='top'>
{getTooltipMessage(isEnabled ? 'Disable Block' : 'Enable Block')}
{(() => {
if (disabled) return getTooltipMessage('Run from block')
if (isExecuting) return 'Execution in progress'
if (!dependenciesSatisfied) return 'Run upstream blocks first'
return 'Run from block'
})()}
</Tooltip.Content>
</Tooltip.Root>
)}
{isSubflowBlock && (
{!isNoteBlock && (
<Tooltip.Root>
<Tooltip.Trigger asChild>
<Button

View File

@@ -40,9 +40,16 @@ export interface BlockMenuProps {
onRemoveFromSubflow: () => void
onOpenEditor: () => void
onRename: () => void
onRunFromBlock?: () => void
onRunUntilBlock?: () => void
hasClipboard?: boolean
showRemoveFromSubflow?: boolean
/** Whether run from block is available (has snapshot, was executed, not inside subflow) */
canRunFromBlock?: boolean
disableEdit?: boolean
isExecuting?: boolean
/** Whether the selected block is a trigger (has no incoming edges) */
isPositionalTrigger?: boolean
}
/**
@@ -65,9 +72,14 @@ export function BlockMenu({
onRemoveFromSubflow,
onOpenEditor,
onRename,
onRunFromBlock,
onRunUntilBlock,
hasClipboard = false,
showRemoveFromSubflow = false,
canRunFromBlock = false,
disableEdit = false,
isExecuting = false,
isPositionalTrigger = false,
}: BlockMenuProps) {
const isSingleBlock = selectedBlocks.length === 1
@@ -78,10 +90,15 @@ export function BlockMenu({
(b) =>
TriggerUtils.requiresSingleInstance(b.type) || TriggerUtils.isSingleInstanceBlockType(b.type)
)
const hasTriggerBlock = selectedBlocks.some((b) => TriggerUtils.isTriggerBlock(b))
// A block is a trigger if it's explicitly a trigger type OR has no incoming edges (positional trigger)
const hasTriggerBlock =
selectedBlocks.some((b) => TriggerUtils.isTriggerBlock(b)) || isPositionalTrigger
const allNoteBlocks = selectedBlocks.every((b) => b.type === 'note')
const isSubflow =
isSingleBlock && (selectedBlocks[0]?.type === 'loop' || selectedBlocks[0]?.type === 'parallel')
const isInsideSubflow =
isSingleBlock &&
(selectedBlocks[0]?.parentType === 'loop' || selectedBlocks[0]?.parentType === 'parallel')
const canRemoveFromSubflow = showRemoveFromSubflow && !hasTriggerBlock
@@ -203,6 +220,38 @@ export function BlockMenu({
</PopoverItem>
)}
{/* Run from/until block - only for single non-note block, not inside subflows */}
{isSingleBlock && !allNoteBlocks && !isInsideSubflow && (
<>
<PopoverDivider />
<PopoverItem
disabled={!canRunFromBlock || isExecuting}
onClick={() => {
if (canRunFromBlock && !isExecuting) {
onRunFromBlock?.()
onClose()
}
}}
>
Run from block
</PopoverItem>
{/* Hide "Run until" for triggers - they're always at the start */}
{!hasTriggerBlock && (
<PopoverItem
disabled={isExecuting}
onClick={() => {
if (!isExecuting) {
onRunUntilBlock?.()
onClose()
}
}}
>
Run until block
</PopoverItem>
)}
</>
)}
{/* Destructive action */}
<PopoverDivider />
<PopoverItem

View File

@@ -1183,19 +1183,6 @@ export const TagDropdown: React.FC<TagDropdownProps> = ({
const outputPaths = getBlockOutputPaths(sourceBlock.type, mergedSubBlocks, true)
blockTags = outputPaths.map((path) => `${normalizedBlockName}.${path}`)
}
} else if (sourceBlock.type === 'approval') {
const dynamicOutputs = getBlockOutputPaths(sourceBlock.type, mergedSubBlocks)
const isSelfReference = activeSourceBlockId === blockId
if (dynamicOutputs.length > 0) {
const allTags = dynamicOutputs.map((path) => `${normalizedBlockName}.${path}`)
blockTags = isSelfReference ? allTags.filter((tag) => tag.endsWith('.url')) : allTags
} else {
const outputPaths = getBlockOutputPaths(sourceBlock.type, mergedSubBlocks)
const allTags = outputPaths.map((path) => `${normalizedBlockName}.${path}`)
blockTags = isSelfReference ? allTags.filter((tag) => tag.endsWith('.url')) : allTags
}
} else if (sourceBlock.type === 'human_in_the_loop') {
const dynamicOutputs = getBlockOutputPaths(sourceBlock.type, mergedSubBlocks)
@@ -1400,13 +1387,8 @@ export const TagDropdown: React.FC<TagDropdownProps> = ({
if (!accessibleBlock) continue
// Skip the current block - blocks cannot reference their own outputs
// Exception: approval and human_in_the_loop blocks can reference their own outputs
if (
accessibleBlockId === blockId &&
accessibleBlock.type !== 'approval' &&
accessibleBlock.type !== 'human_in_the_loop'
)
continue
// Exception: human_in_the_loop blocks can reference their own outputs (url, resumeEndpoint)
if (accessibleBlockId === blockId && accessibleBlock.type !== 'human_in_the_loop') continue
const blockConfig = getBlock(accessibleBlock.type)
@@ -1520,19 +1502,6 @@ export const TagDropdown: React.FC<TagDropdownProps> = ({
const outputPaths = getBlockOutputPaths(accessibleBlock.type, mergedSubBlocks, true)
blockTags = outputPaths.map((path) => `${normalizedBlockName}.${path}`)
}
} else if (accessibleBlock.type === 'approval') {
const dynamicOutputs = getBlockOutputPaths(accessibleBlock.type, mergedSubBlocks)
const isSelfReference = accessibleBlockId === blockId
if (dynamicOutputs.length > 0) {
const allTags = dynamicOutputs.map((path) => `${normalizedBlockName}.${path}`)
blockTags = isSelfReference ? allTags.filter((tag) => tag.endsWith('.url')) : allTags
} else {
const outputPaths = getBlockOutputPaths(accessibleBlock.type, mergedSubBlocks)
const allTags = outputPaths.map((path) => `${normalizedBlockName}.${path}`)
blockTags = isSelfReference ? allTags.filter((tag) => tag.endsWith('.url')) : allTags
}
} else if (accessibleBlock.type === 'human_in_the_loop') {
const dynamicOutputs = getBlockOutputPaths(accessibleBlock.type, mergedSubBlocks)

View File

@@ -15,13 +15,16 @@ import {
TriggerUtils,
} from '@/lib/workflows/triggers/triggers'
import { useCurrentWorkflow } from '@/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-current-workflow'
import type { BlockLog, ExecutionResult, StreamingExecution } from '@/executor/types'
import { getBlock } from '@/blocks'
import type { SerializableExecutionState } from '@/executor/execution/types'
import type { BlockLog, BlockState, ExecutionResult, StreamingExecution } from '@/executor/types'
import { hasExecutionResult } from '@/executor/utils/errors'
import { coerceValue } from '@/executor/utils/start-block'
import { subscriptionKeys } from '@/hooks/queries/subscription'
import { useExecutionStream } from '@/hooks/use-execution-stream'
import { WorkflowValidationError } from '@/serializer'
import { useExecutionStore } from '@/stores/execution'
import { useNotificationStore } from '@/stores/notifications'
import { useVariablesStore } from '@/stores/panel'
import { useEnvironmentStore } from '@/stores/settings/environment'
import { type ConsoleEntry, useTerminalConsoleStore } from '@/stores/terminal'
@@ -98,11 +101,15 @@ export function useWorkflowExecution() {
setActiveBlocks,
setBlockRunStatus,
setEdgeRunStatus,
setLastExecutionSnapshot,
getLastExecutionSnapshot,
clearLastExecutionSnapshot,
} = useExecutionStore()
const [executionResult, setExecutionResult] = useState<ExecutionResult | null>(null)
const executionStream = useExecutionStream()
const currentChatExecutionIdRef = useRef<string | null>(null)
const isViewingDiff = useWorkflowDiffStore((state) => state.isShowingDiff)
const addNotification = useNotificationStore((state) => state.addNotification)
/**
* Validates debug state before performing debug operations
@@ -668,7 +675,8 @@ export function useWorkflowExecution() {
onStream?: (se: StreamingExecution) => Promise<void>,
executionId?: string,
onBlockComplete?: (blockId: string, output: any) => Promise<void>,
overrideTriggerType?: 'chat' | 'manual' | 'api'
overrideTriggerType?: 'chat' | 'manual' | 'api',
stopAfterBlockId?: string
): Promise<ExecutionResult | StreamingExecution> => {
// Use diff workflow for execution when available, regardless of canvas view state
const executionWorkflowState = null as {
@@ -876,6 +884,8 @@ export function useWorkflowExecution() {
const activeBlocksSet = new Set<string>()
const streamedContent = new Map<string, string>()
const accumulatedBlockLogs: BlockLog[] = []
const accumulatedBlockStates = new Map<string, BlockState>()
const executedBlockIds = new Set<string>()
// Execute the workflow
try {
@@ -887,6 +897,7 @@ export function useWorkflowExecution() {
triggerType: overrideTriggerType || 'manual',
useDraftState: true,
isClientSession: true,
stopAfterBlockId,
workflowStateOverride: executionWorkflowState
? {
blocks: executionWorkflowState.blocks,
@@ -916,18 +927,22 @@ export function useWorkflowExecution() {
logger.info('onBlockCompleted received:', { data })
activeBlocksSet.delete(data.blockId)
// Create a new Set to trigger React re-render
setActiveBlocks(new Set(activeBlocksSet))
// Track successful block execution in run path
setBlockRunStatus(data.blockId, 'success')
// Edges already tracked in onBlockStarted, no need to track again
executedBlockIds.add(data.blockId)
accumulatedBlockStates.set(data.blockId, {
output: data.output,
executed: true,
executionTime: data.durationMs,
})
const isContainerBlock = data.blockType === 'loop' || data.blockType === 'parallel'
if (isContainerBlock) return
const startedAt = new Date(Date.now() - data.durationMs).toISOString()
const endedAt = new Date().toISOString()
// Accumulate block log for the execution result
accumulatedBlockLogs.push({
blockId: data.blockId,
blockName: data.blockName || 'Unknown Block',
@@ -1056,6 +1071,53 @@ export function useWorkflowExecution() {
},
logs: accumulatedBlockLogs,
}
// Add trigger block to executed blocks so downstream blocks can use run-from-block
if (data.success && startBlockId) {
executedBlockIds.add(startBlockId)
}
if (data.success && activeWorkflowId) {
if (stopAfterBlockId) {
const existingSnapshot = getLastExecutionSnapshot(activeWorkflowId)
const mergedBlockStates = {
...(existingSnapshot?.blockStates || {}),
...Object.fromEntries(accumulatedBlockStates),
}
const mergedExecutedBlocks = new Set([
...(existingSnapshot?.executedBlocks || []),
...executedBlockIds,
])
const snapshot: SerializableExecutionState = {
blockStates: mergedBlockStates,
executedBlocks: Array.from(mergedExecutedBlocks),
blockLogs: [...(existingSnapshot?.blockLogs || []), ...accumulatedBlockLogs],
decisions: existingSnapshot?.decisions || { router: {}, condition: {} },
completedLoops: existingSnapshot?.completedLoops || [],
activeExecutionPath: Array.from(mergedExecutedBlocks),
}
setLastExecutionSnapshot(activeWorkflowId, snapshot)
logger.info('Merged execution snapshot after run-until-block', {
workflowId: activeWorkflowId,
newBlocksExecuted: executedBlockIds.size,
totalExecutedBlocks: mergedExecutedBlocks.size,
})
} else {
const snapshot: SerializableExecutionState = {
blockStates: Object.fromEntries(accumulatedBlockStates),
executedBlocks: Array.from(executedBlockIds),
blockLogs: accumulatedBlockLogs,
decisions: { router: {}, condition: {} },
completedLoops: [],
activeExecutionPath: Array.from(executedBlockIds),
}
setLastExecutionSnapshot(activeWorkflowId, snapshot)
logger.info('Stored execution snapshot for run-from-block', {
workflowId: activeWorkflowId,
executedBlocksCount: executedBlockIds.size,
})
}
}
},
onExecutionError: (data) => {
@@ -1376,6 +1438,330 @@ export function useWorkflowExecution() {
setActiveBlocks,
])
/**
* Handles running workflow from a specific block using cached outputs
*/
const handleRunFromBlock = useCallback(
async (blockId: string, workflowId: string) => {
const snapshot = getLastExecutionSnapshot(workflowId)
const workflowEdges = useWorkflowStore.getState().edges
const incomingEdges = workflowEdges.filter((edge) => edge.target === blockId)
const isTriggerBlock = incomingEdges.length === 0
// Check if each source block is either executed OR is a trigger block (triggers don't need prior execution)
const isSourceSatisfied = (sourceId: string) => {
if (snapshot?.executedBlocks.includes(sourceId)) return true
// Check if source is a trigger (has no incoming edges itself)
const sourceIncomingEdges = workflowEdges.filter((edge) => edge.target === sourceId)
return sourceIncomingEdges.length === 0
}
// Non-trigger blocks need a snapshot to exist (so upstream outputs are available)
if (!snapshot && !isTriggerBlock) {
logger.error('No execution snapshot available for run-from-block', { workflowId, blockId })
return
}
const dependenciesSatisfied =
isTriggerBlock || incomingEdges.every((edge) => isSourceSatisfied(edge.source))
if (!dependenciesSatisfied) {
logger.error('Upstream dependencies not satisfied for run-from-block', {
workflowId,
blockId,
})
return
}
// For trigger blocks, always use empty snapshot to prevent stale data from different
// execution paths from being resolved. For non-trigger blocks, use the existing snapshot.
const emptySnapshot: SerializableExecutionState = {
blockStates: {},
executedBlocks: [],
blockLogs: [],
decisions: { router: {}, condition: {} },
completedLoops: [],
activeExecutionPath: [],
}
const effectiveSnapshot: SerializableExecutionState = isTriggerBlock
? emptySnapshot
: snapshot || emptySnapshot
// Extract mock payload for trigger blocks
let workflowInput: any
if (isTriggerBlock) {
const workflowBlocks = useWorkflowStore.getState().blocks
const mergedStates = mergeSubblockState(workflowBlocks, workflowId)
const candidates = resolveStartCandidates(mergedStates, { execution: 'manual' })
const candidate = candidates.find((c) => c.blockId === blockId)
if (candidate) {
if (triggerNeedsMockPayload(candidate)) {
workflowInput = extractTriggerMockPayload(candidate)
} else if (
candidate.path === StartBlockPath.SPLIT_API ||
candidate.path === StartBlockPath.SPLIT_INPUT ||
candidate.path === StartBlockPath.UNIFIED
) {
const inputFormatValue = candidate.block.subBlocks?.inputFormat?.value
if (Array.isArray(inputFormatValue)) {
const testInput: Record<string, any> = {}
inputFormatValue.forEach((field: any) => {
if (field && typeof field === 'object' && field.name && field.value !== undefined) {
testInput[field.name] = coerceValue(field.type, field.value)
}
})
if (Object.keys(testInput).length > 0) {
workflowInput = testInput
}
}
}
} else {
// Fallback: block is trigger by position but not classified as start candidate
const block = mergedStates[blockId]
if (block) {
const blockConfig = getBlock(block.type)
const hasTriggers = blockConfig?.triggers?.available?.length
if (hasTriggers || block.triggerMode) {
workflowInput = extractTriggerMockPayload({
blockId,
block,
path: StartBlockPath.EXTERNAL_TRIGGER,
})
}
}
}
}
setIsExecuting(true)
const executionId = uuidv4()
const accumulatedBlockLogs: BlockLog[] = []
const accumulatedBlockStates = new Map<string, BlockState>()
const executedBlockIds = new Set<string>()
const activeBlocksSet = new Set<string>()
try {
await executionStream.executeFromBlock({
workflowId,
startBlockId: blockId,
sourceSnapshot: effectiveSnapshot,
input: workflowInput,
callbacks: {
onBlockStarted: (data) => {
activeBlocksSet.add(data.blockId)
setActiveBlocks(new Set(activeBlocksSet))
const incomingEdges = workflowEdges.filter((edge) => edge.target === data.blockId)
incomingEdges.forEach((edge) => {
setEdgeRunStatus(edge.id, 'success')
})
},
onBlockCompleted: (data) => {
activeBlocksSet.delete(data.blockId)
setActiveBlocks(new Set(activeBlocksSet))
setBlockRunStatus(data.blockId, 'success')
executedBlockIds.add(data.blockId)
accumulatedBlockStates.set(data.blockId, {
output: data.output,
executed: true,
executionTime: data.durationMs,
})
const isContainerBlock = data.blockType === 'loop' || data.blockType === 'parallel'
if (isContainerBlock) return
const startedAt = new Date(Date.now() - data.durationMs).toISOString()
const endedAt = new Date().toISOString()
accumulatedBlockLogs.push({
blockId: data.blockId,
blockName: data.blockName || 'Unknown Block',
blockType: data.blockType || 'unknown',
input: data.input || {},
output: data.output,
success: true,
durationMs: data.durationMs,
startedAt,
endedAt,
})
addConsole({
input: data.input || {},
output: data.output,
success: true,
durationMs: data.durationMs,
startedAt,
endedAt,
workflowId,
blockId: data.blockId,
executionId,
blockName: data.blockName || 'Unknown Block',
blockType: data.blockType || 'unknown',
iterationCurrent: data.iterationCurrent,
iterationTotal: data.iterationTotal,
iterationType: data.iterationType,
})
},
onBlockError: (data) => {
activeBlocksSet.delete(data.blockId)
setActiveBlocks(new Set(activeBlocksSet))
setBlockRunStatus(data.blockId, 'error')
const startedAt = new Date(Date.now() - data.durationMs).toISOString()
const endedAt = new Date().toISOString()
accumulatedBlockLogs.push({
blockId: data.blockId,
blockName: data.blockName || 'Unknown Block',
blockType: data.blockType || 'unknown',
input: data.input || {},
output: {},
success: false,
error: data.error,
durationMs: data.durationMs,
startedAt,
endedAt,
})
addConsole({
input: data.input || {},
output: {},
success: false,
error: data.error,
durationMs: data.durationMs,
startedAt,
endedAt,
workflowId,
blockId: data.blockId,
executionId,
blockName: data.blockName,
blockType: data.blockType,
iterationCurrent: data.iterationCurrent,
iterationTotal: data.iterationTotal,
iterationType: data.iterationType,
})
},
onExecutionCompleted: (data) => {
if (data.success) {
// Add the start block (trigger) to executed blocks
executedBlockIds.add(blockId)
const mergedBlockStates: Record<string, BlockState> = {
...effectiveSnapshot.blockStates,
}
for (const [bId, state] of accumulatedBlockStates) {
mergedBlockStates[bId] = state
}
const mergedExecutedBlocks = new Set([
...effectiveSnapshot.executedBlocks,
...executedBlockIds,
])
const updatedSnapshot: SerializableExecutionState = {
...effectiveSnapshot,
blockStates: mergedBlockStates,
executedBlocks: Array.from(mergedExecutedBlocks),
blockLogs: [...effectiveSnapshot.blockLogs, ...accumulatedBlockLogs],
activeExecutionPath: Array.from(mergedExecutedBlocks),
}
setLastExecutionSnapshot(workflowId, updatedSnapshot)
}
},
onExecutionError: (data) => {
const isWorkflowModified =
data.error?.includes('Block not found in workflow') ||
data.error?.includes('Upstream dependency not executed')
if (isWorkflowModified) {
clearLastExecutionSnapshot(workflowId)
addNotification({
level: 'error',
message:
'Workflow was modified. Run the workflow again to enable running from block.',
workflowId,
})
} else {
addNotification({
level: 'error',
message: data.error || 'Run from block failed',
workflowId,
})
}
},
},
})
} catch (error) {
if ((error as Error).name !== 'AbortError') {
logger.error('Run-from-block failed:', error)
}
} finally {
setIsExecuting(false)
setActiveBlocks(new Set())
}
},
[
getLastExecutionSnapshot,
setLastExecutionSnapshot,
clearLastExecutionSnapshot,
setIsExecuting,
setActiveBlocks,
setBlockRunStatus,
setEdgeRunStatus,
addNotification,
addConsole,
executionStream,
]
)
/**
* Handles running workflow until a specific block (stops after that block completes)
*/
const handleRunUntilBlock = useCallback(
async (blockId: string, workflowId: string) => {
if (!workflowId || workflowId !== activeWorkflowId) {
logger.error('Invalid workflow ID for run-until-block', { workflowId, activeWorkflowId })
return
}
logger.info('Starting run-until-block execution', { workflowId, stopAfterBlockId: blockId })
setExecutionResult(null)
setIsExecuting(true)
const executionId = uuidv4()
try {
const result = await executeWorkflow(
undefined,
undefined,
executionId,
undefined,
'manual',
blockId
)
if (result && 'success' in result) {
setExecutionResult(result)
}
} catch (error) {
const errorResult = handleExecutionError(error, { executionId })
return errorResult
} finally {
setIsExecuting(false)
setIsDebugging(false)
setActiveBlocks(new Set())
}
},
[activeWorkflowId, setExecutionResult, setIsExecuting, setIsDebugging, setActiveBlocks]
)
return {
isExecuting,
isDebugging,
@@ -1386,5 +1772,7 @@ export function useWorkflowExecution() {
handleResumeDebug,
handleCancelDebug,
handleCancelExecution,
handleRunFromBlock,
handleRunUntilBlock,
}
}

View File

@@ -47,6 +47,7 @@ import {
useCurrentWorkflow,
useNodeUtilities,
useShiftSelectionLock,
useWorkflowExecution,
} from '@/app/workspace/[workspaceId]/w/[workflowId]/hooks'
import {
calculateContainerDimensions,
@@ -302,6 +303,8 @@ const WorkflowContent = React.memo(() => {
const showTrainingModal = useCopilotTrainingStore((state) => state.showModal)
const { handleRunFromBlock, handleRunUntilBlock } = useWorkflowExecution()
const snapToGridSize = useSnapToGridSize()
const snapToGrid = snapToGridSize > 0
@@ -733,13 +736,16 @@ const WorkflowContent = React.memo(() => {
[collaborativeBatchAddBlocks, setSelectedEdges, setPendingSelection]
)
const { activeBlockIds, pendingBlocks, isDebugging } = useExecutionStore(
useShallow((state) => ({
activeBlockIds: state.activeBlockIds,
pendingBlocks: state.pendingBlocks,
isDebugging: state.isDebugging,
}))
)
const { activeBlockIds, pendingBlocks, isDebugging, isExecuting, getLastExecutionSnapshot } =
useExecutionStore(
useShallow((state) => ({
activeBlockIds: state.activeBlockIds,
pendingBlocks: state.pendingBlocks,
isDebugging: state.isDebugging,
isExecuting: state.isExecuting,
getLastExecutionSnapshot: state.getLastExecutionSnapshot,
}))
)
const [dragStartParentId, setDragStartParentId] = useState<string | null>(null)
@@ -1102,6 +1108,50 @@ const WorkflowContent = React.memo(() => {
}
}, [contextMenuBlocks])
const handleContextRunFromBlock = useCallback(() => {
if (contextMenuBlocks.length !== 1) return
const blockId = contextMenuBlocks[0].id
handleRunFromBlock(blockId, workflowIdParam)
}, [contextMenuBlocks, workflowIdParam, handleRunFromBlock])
const handleContextRunUntilBlock = useCallback(() => {
if (contextMenuBlocks.length !== 1) return
const blockId = contextMenuBlocks[0].id
handleRunUntilBlock(blockId, workflowIdParam)
}, [contextMenuBlocks, workflowIdParam, handleRunUntilBlock])
const runFromBlockState = useMemo(() => {
if (contextMenuBlocks.length !== 1) {
return { canRun: false, reason: undefined }
}
const block = contextMenuBlocks[0]
const snapshot = getLastExecutionSnapshot(workflowIdParam)
const incomingEdges = edges.filter((edge) => edge.target === block.id)
const isTriggerBlock = incomingEdges.length === 0
// Check if each source block is either executed OR is a trigger block (triggers don't need prior execution)
const isSourceSatisfied = (sourceId: string) => {
if (snapshot?.executedBlocks.includes(sourceId)) return true
// Check if source is a trigger (has no incoming edges itself)
const sourceIncomingEdges = edges.filter((edge) => edge.target === sourceId)
return sourceIncomingEdges.length === 0
}
// Non-trigger blocks need a snapshot to exist (so upstream outputs are available)
const dependenciesSatisfied =
isTriggerBlock || (snapshot && incomingEdges.every((edge) => isSourceSatisfied(edge.source)))
const isNoteBlock = block.type === 'note'
const isInsideSubflow =
block.parentId && (block.parentType === 'loop' || block.parentType === 'parallel')
if (isInsideSubflow) return { canRun: false, reason: 'Cannot run from inside subflow' }
if (!dependenciesSatisfied) return { canRun: false, reason: 'Run upstream blocks first' }
if (isNoteBlock) return { canRun: false, reason: undefined }
if (isExecuting) return { canRun: false, reason: undefined }
return { canRun: true, reason: undefined }
}, [contextMenuBlocks, edges, workflowIdParam, getLastExecutionSnapshot, isExecuting])
const handleContextAddBlock = useCallback(() => {
useSearchModalStore.getState().open()
}, [])
@@ -3418,11 +3468,19 @@ const WorkflowContent = React.memo(() => {
onRemoveFromSubflow={handleContextRemoveFromSubflow}
onOpenEditor={handleContextOpenEditor}
onRename={handleContextRename}
onRunFromBlock={handleContextRunFromBlock}
onRunUntilBlock={handleContextRunUntilBlock}
hasClipboard={hasClipboard()}
showRemoveFromSubflow={contextMenuBlocks.some(
(b) => b.parentId && (b.parentType === 'loop' || b.parentType === 'parallel')
)}
canRunFromBlock={runFromBlockState.canRun}
disableEdit={!effectivePermissions.canEdit}
isExecuting={isExecuting}
isPositionalTrigger={
contextMenuBlocks.length === 1 &&
edges.filter((e) => e.target === contextMenuBlocks[0]?.id).length === 0
}
/>
<CanvasMenu

View File

@@ -769,7 +769,13 @@ Example 3 (Array Input):
outputs: {
content: { type: 'string', description: 'Generated response content' },
model: { type: 'string', description: 'Model used for generation' },
tokens: { type: 'any', description: 'Token usage statistics' },
toolCalls: { type: 'any', description: 'Tool calls made' },
tokens: { type: 'json', description: 'Token usage statistics' },
toolCalls: { type: 'json', description: 'Tool calls made' },
providerTiming: {
type: 'json',
description: 'Provider timing information',
hiddenFromDisplay: true,
},
cost: { type: 'number', description: 'Cost of the API call', hiddenFromDisplay: true },
},
}

View File

@@ -0,0 +1,393 @@
import { ClerkIcon } from '@/components/icons'
import type { BlockConfig } from '@/blocks/types'
import type { ClerkResponse } from '@/tools/clerk/types'
export const ClerkBlock: BlockConfig<ClerkResponse> = {
type: 'clerk',
name: 'Clerk',
description: 'Manage users, organizations, and sessions in Clerk',
longDescription:
'Integrate Clerk authentication and user management into your workflow. Create, update, delete, and list users. Manage organizations and their memberships. Monitor and control user sessions.',
docsLink: 'https://docs.sim.ai/tools/clerk',
category: 'tools',
bgColor: '#131316',
icon: ClerkIcon,
subBlocks: [
{
id: 'operation',
title: 'Operation',
type: 'dropdown',
options: [
{ label: 'List Users', id: 'clerk_list_users' },
{ label: 'Get User', id: 'clerk_get_user' },
{ label: 'Create User', id: 'clerk_create_user' },
{ label: 'Update User', id: 'clerk_update_user' },
{ label: 'Delete User', id: 'clerk_delete_user' },
{ label: 'List Organizations', id: 'clerk_list_organizations' },
{ label: 'Get Organization', id: 'clerk_get_organization' },
{ label: 'Create Organization', id: 'clerk_create_organization' },
{ label: 'List Sessions', id: 'clerk_list_sessions' },
{ label: 'Get Session', id: 'clerk_get_session' },
{ label: 'Revoke Session', id: 'clerk_revoke_session' },
],
value: () => 'clerk_list_users',
},
{
id: 'secretKey',
title: 'Secret Key',
type: 'short-input',
password: true,
placeholder: 'sk_live_... or sk_test_...',
required: true,
},
// List Users params
{
id: 'query',
title: 'Search Query',
type: 'short-input',
placeholder: 'Search by email, phone, username, or name',
condition: { field: 'operation', value: 'clerk_list_users' },
},
{
id: 'emailAddressFilter',
title: 'Email Filter',
type: 'short-input',
placeholder: 'Filter by email (comma-separated)',
condition: { field: 'operation', value: 'clerk_list_users' },
},
{
id: 'usernameFilter',
title: 'Username Filter',
type: 'short-input',
placeholder: 'Filter by username (comma-separated)',
condition: { field: 'operation', value: 'clerk_list_users' },
},
// Get User params
{
id: 'userId',
title: 'User ID',
type: 'short-input',
placeholder: 'user_...',
condition: {
field: 'operation',
value: ['clerk_get_user', 'clerk_update_user', 'clerk_delete_user'],
},
required: {
field: 'operation',
value: ['clerk_get_user', 'clerk_update_user', 'clerk_delete_user'],
},
},
// Create/Update User params
{
id: 'emailAddress',
title: 'Email Address',
type: 'short-input',
placeholder: 'user@example.com (comma-separated for multiple)',
condition: { field: 'operation', value: 'clerk_create_user' },
},
{
id: 'phoneNumber',
title: 'Phone Number',
type: 'short-input',
placeholder: '+1234567890 (comma-separated for multiple)',
condition: { field: 'operation', value: 'clerk_create_user' },
},
{
id: 'username',
title: 'Username',
type: 'short-input',
placeholder: 'johndoe',
condition: { field: 'operation', value: ['clerk_create_user', 'clerk_update_user'] },
},
{
id: 'password',
title: 'Password',
type: 'short-input',
password: true,
placeholder: 'Minimum 8 characters',
condition: { field: 'operation', value: ['clerk_create_user', 'clerk_update_user'] },
},
{
id: 'firstName',
title: 'First Name',
type: 'short-input',
placeholder: 'John',
condition: { field: 'operation', value: ['clerk_create_user', 'clerk_update_user'] },
},
{
id: 'lastName',
title: 'Last Name',
type: 'short-input',
placeholder: 'Doe',
condition: { field: 'operation', value: ['clerk_create_user', 'clerk_update_user'] },
},
{
id: 'externalId',
title: 'External ID',
type: 'short-input',
placeholder: 'Your system user ID',
condition: { field: 'operation', value: ['clerk_create_user', 'clerk_update_user'] },
},
{
id: 'publicMetadata',
title: 'Public Metadata',
type: 'code',
language: 'json',
placeholder: '{"role": "admin"}',
condition: { field: 'operation', value: ['clerk_create_user', 'clerk_update_user'] },
},
{
id: 'privateMetadata',
title: 'Private Metadata',
type: 'code',
language: 'json',
placeholder: '{"internalId": "123"}',
condition: { field: 'operation', value: ['clerk_create_user', 'clerk_update_user'] },
},
// Organization params
{
id: 'orgQuery',
title: 'Search Query',
type: 'short-input',
placeholder: 'Search by name, ID, or slug',
condition: { field: 'operation', value: 'clerk_list_organizations' },
},
{
id: 'includeMembersCount',
title: 'Include Members Count',
type: 'switch',
condition: { field: 'operation', value: 'clerk_list_organizations' },
},
{
id: 'organizationId',
title: 'Organization ID',
type: 'short-input',
placeholder: 'org_... or slug',
condition: { field: 'operation', value: 'clerk_get_organization' },
required: { field: 'operation', value: 'clerk_get_organization' },
},
{
id: 'orgName',
title: 'Organization Name',
type: 'short-input',
placeholder: 'Acme Corp',
condition: { field: 'operation', value: 'clerk_create_organization' },
required: { field: 'operation', value: 'clerk_create_organization' },
},
{
id: 'createdBy',
title: 'Creator User ID',
type: 'short-input',
placeholder: 'user_... (will become admin)',
condition: { field: 'operation', value: 'clerk_create_organization' },
required: { field: 'operation', value: 'clerk_create_organization' },
},
{
id: 'slug',
title: 'Slug',
type: 'short-input',
placeholder: 'acme-corp',
condition: { field: 'operation', value: 'clerk_create_organization' },
},
{
id: 'maxAllowedMemberships',
title: 'Max Members',
type: 'short-input',
placeholder: '0 for unlimited',
condition: { field: 'operation', value: 'clerk_create_organization' },
},
// Session params
{
id: 'sessionUserId',
title: 'User ID',
type: 'short-input',
placeholder: 'user_...',
condition: { field: 'operation', value: 'clerk_list_sessions' },
},
{
id: 'clientId',
title: 'Client ID',
type: 'short-input',
placeholder: 'client_...',
condition: { field: 'operation', value: 'clerk_list_sessions' },
},
{
id: 'sessionStatus',
title: 'Status',
type: 'dropdown',
options: [
{ label: 'All', id: '' },
{ label: 'Active', id: 'active' },
{ label: 'Ended', id: 'ended' },
{ label: 'Expired', id: 'expired' },
{ label: 'Revoked', id: 'revoked' },
{ label: 'Abandoned', id: 'abandoned' },
{ label: 'Pending', id: 'pending' },
],
value: () => '',
condition: { field: 'operation', value: 'clerk_list_sessions' },
},
{
id: 'sessionId',
title: 'Session ID',
type: 'short-input',
placeholder: 'sess_...',
condition: { field: 'operation', value: ['clerk_get_session', 'clerk_revoke_session'] },
required: { field: 'operation', value: ['clerk_get_session', 'clerk_revoke_session'] },
},
// Pagination params (common)
{
id: 'limit',
title: 'Limit',
type: 'short-input',
placeholder: 'Results per page (1-500, default: 10)',
condition: {
field: 'operation',
value: ['clerk_list_users', 'clerk_list_organizations', 'clerk_list_sessions'],
},
},
{
id: 'offset',
title: 'Offset',
type: 'short-input',
placeholder: 'Skip N results for pagination',
condition: {
field: 'operation',
value: ['clerk_list_users', 'clerk_list_organizations', 'clerk_list_sessions'],
},
},
],
tools: {
access: [
'clerk_list_users',
'clerk_get_user',
'clerk_create_user',
'clerk_update_user',
'clerk_delete_user',
'clerk_list_organizations',
'clerk_get_organization',
'clerk_create_organization',
'clerk_list_sessions',
'clerk_get_session',
'clerk_revoke_session',
],
config: {
tool: (params) => params.operation as string,
params: (params) => {
const {
operation,
secretKey,
emailAddressFilter,
usernameFilter,
orgQuery,
orgName,
sessionUserId,
sessionStatus,
publicMetadata,
privateMetadata,
maxAllowedMemberships,
...rest
} = params
const cleanParams: Record<string, unknown> = {
secretKey,
}
// Map UI fields to API params based on operation
switch (operation) {
case 'clerk_list_users':
if (emailAddressFilter) cleanParams.emailAddress = emailAddressFilter
if (usernameFilter) cleanParams.username = usernameFilter
break
case 'clerk_create_user':
case 'clerk_update_user':
if (publicMetadata) {
cleanParams.publicMetadata =
typeof publicMetadata === 'string' ? JSON.parse(publicMetadata) : publicMetadata
}
if (privateMetadata) {
cleanParams.privateMetadata =
typeof privateMetadata === 'string' ? JSON.parse(privateMetadata) : privateMetadata
}
break
case 'clerk_list_organizations':
if (orgQuery) cleanParams.query = orgQuery
break
case 'clerk_create_organization':
if (orgName) cleanParams.name = orgName
if (maxAllowedMemberships)
cleanParams.maxAllowedMemberships = Number(maxAllowedMemberships)
break
case 'clerk_list_sessions':
if (sessionUserId) cleanParams.userId = sessionUserId
if (sessionStatus) cleanParams.status = sessionStatus
break
}
// Add remaining params that don't need mapping
Object.entries(rest).forEach(([key, value]) => {
if (value !== undefined && value !== null && value !== '') {
cleanParams[key] = value
}
})
return cleanParams
},
},
},
inputs: {
operation: { type: 'string', description: 'Operation to perform' },
secretKey: { type: 'string', description: 'Clerk Secret Key' },
userId: { type: 'string', description: 'User ID' },
organizationId: { type: 'string', description: 'Organization ID or slug' },
sessionId: { type: 'string', description: 'Session ID' },
query: { type: 'string', description: 'Search query' },
limit: { type: 'number', description: 'Results per page' },
offset: { type: 'number', description: 'Pagination offset' },
},
outputs: {
// List outputs (arrays stored as json for block compatibility)
users: { type: 'json', description: 'Array of user objects' },
organizations: { type: 'json', description: 'Array of organization objects' },
sessions: { type: 'json', description: 'Array of session objects' },
// Single entity fields (destructured from get/create/update operations)
id: { type: 'string', description: 'Resource ID (user, organization, or session)' },
name: { type: 'string', description: 'Organization name' },
slug: { type: 'string', description: 'Organization slug' },
username: { type: 'string', description: 'Username' },
firstName: { type: 'string', description: 'First name' },
lastName: { type: 'string', description: 'Last name' },
imageUrl: { type: 'string', description: 'Profile image URL' },
hasImage: { type: 'boolean', description: 'Whether resource has an image' },
emailAddresses: { type: 'json', description: 'User email addresses' },
phoneNumbers: { type: 'json', description: 'User phone numbers' },
primaryEmailAddressId: { type: 'string', description: 'Primary email address ID' },
primaryPhoneNumberId: { type: 'string', description: 'Primary phone number ID' },
externalId: { type: 'string', description: 'External system ID' },
passwordEnabled: { type: 'boolean', description: 'Whether password is enabled' },
twoFactorEnabled: { type: 'boolean', description: 'Whether 2FA is enabled' },
banned: { type: 'boolean', description: 'Whether user is banned' },
locked: { type: 'boolean', description: 'Whether user is locked' },
userId: { type: 'string', description: 'User ID (for sessions)' },
clientId: { type: 'string', description: 'Client ID (for sessions)' },
status: { type: 'string', description: 'Session status' },
lastActiveAt: { type: 'number', description: 'Last activity timestamp' },
lastSignInAt: { type: 'number', description: 'Last sign-in timestamp' },
membersCount: { type: 'number', description: 'Number of members' },
maxAllowedMemberships: { type: 'number', description: 'Max allowed memberships' },
adminDeleteEnabled: { type: 'boolean', description: 'Whether admin delete is enabled' },
createdBy: { type: 'string', description: 'Creator user ID' },
publicMetadata: { type: 'json', description: 'Public metadata' },
// Common outputs
totalCount: { type: 'number', description: 'Total count for paginated results' },
deleted: { type: 'boolean', description: 'Whether the resource was deleted' },
object: { type: 'string', description: 'Object type' },
createdAt: { type: 'number', description: 'Creation timestamp' },
updatedAt: { type: 'number', description: 'Last update timestamp' },
success: { type: 'boolean', description: 'Operation success status' },
},
}

View File

@@ -162,5 +162,21 @@ export const HumanInTheLoopBlock: BlockConfig<ResponseBlockOutput> = {
type: 'string',
description: 'Resume API endpoint URL for direct curl requests',
},
response: {
type: 'json',
description: 'Display data shown to the approver',
hiddenFromDisplay: true,
},
submission: {
type: 'json',
description: 'Form submission data from the approver',
hiddenFromDisplay: true,
},
resumeInput: {
type: 'json',
description: 'Raw input data submitted when resuming',
hiddenFromDisplay: true,
},
submittedAt: { type: 'string', description: 'ISO timestamp when the workflow was resumed' },
},
}

View File

@@ -247,6 +247,7 @@ export const RouterBlock: BlockConfig<RouterResponse> = {
tokens: { type: 'json', description: 'Token usage' },
cost: { type: 'json', description: 'Cost information' },
selectedPath: { type: 'json', description: 'Selected routing path' },
selectedRoute: { type: 'string', description: 'Selected route ID' },
},
}

View File

@@ -44,6 +44,11 @@ export const WorkflowBlock: BlockConfig = {
childWorkflowName: { type: 'string', description: 'Child workflow name' },
result: { type: 'json', description: 'Workflow execution result' },
error: { type: 'string', description: 'Error message' },
childTraceSpans: {
type: 'json',
description: 'Child workflow trace spans',
hiddenFromDisplay: true,
},
},
hideFromToolbar: true,
}

View File

@@ -43,5 +43,10 @@ export const WorkflowInputBlock: BlockConfig = {
childWorkflowName: { type: 'string', description: 'Child workflow name' },
result: { type: 'json', description: 'Workflow execution result' },
error: { type: 'string', description: 'Error message' },
childTraceSpans: {
type: 'json',
description: 'Child workflow trace spans',
hiddenFromDisplay: true,
},
},
}

View File

@@ -13,6 +13,7 @@ import { CalendlyBlock } from '@/blocks/blocks/calendly'
import { ChatTriggerBlock } from '@/blocks/blocks/chat_trigger'
import { CirclebackBlock } from '@/blocks/blocks/circleback'
import { ClayBlock } from '@/blocks/blocks/clay'
import { ClerkBlock } from '@/blocks/blocks/clerk'
import { ConditionBlock } from '@/blocks/blocks/condition'
import { ConfluenceBlock, ConfluenceV2Block } from '@/blocks/blocks/confluence'
import { CursorBlock, CursorV2Block } from '@/blocks/blocks/cursor'
@@ -168,6 +169,7 @@ export const registry: Record<string, BlockConfig> = {
chat_trigger: ChatTriggerBlock,
circleback: CirclebackBlock,
clay: ClayBlock,
clerk: ClerkBlock,
condition: ConditionBlock,
confluence: ConfluenceBlock,
confluence_v2: ConfluenceV2Block,

View File

@@ -157,8 +157,19 @@ export type OutputFieldDefinition =
* Uses the same condition format as subBlocks.
*/
condition?: OutputCondition
/**
* If true, this output is hidden from display in the tag dropdown and logs,
* but still available for resolution and execution.
*/
hiddenFromDisplay?: boolean
}
export function isHiddenFromDisplay(def: unknown): boolean {
return Boolean(
def && typeof def === 'object' && 'hiddenFromDisplay' in def && def.hiddenFromDisplay
)
}
export interface ParamConfig {
type: ParamType
description?: string

View File

@@ -2096,6 +2096,23 @@ export function ClayIcon(props: SVGProps<SVGSVGElement>) {
)
}
export function ClerkIcon(props: SVGProps<SVGSVGElement>) {
return (
<svg {...props} viewBox='0 0 128 128' fill='none' xmlns='http://www.w3.org/2000/svg'>
<circle cx='64' cy='64' r='20' fill='white' />
<path
d='M99.5716 10.788C101.571 12.1272 101.742 14.9444 100.04 16.646L85.4244 31.2618C84.1035 32.5828 82.0542 32.7914 80.3915 31.9397C75.4752 29.421 69.9035 28 64 28C44.1177 28 28 44.1177 28 64C28 69.9035 29.421 75.4752 31.9397 80.3915C32.7914 82.0542 32.5828 84.1035 31.2618 85.4244L16.646 100.04C14.9444 101.742 12.1272 101.571 10.788 99.5716C3.97411 89.3989 0 77.1635 0 64C0 28.6538 28.6538 0 64 0C77.1635 0 89.3989 3.97411 99.5716 10.788Z'
fill='white'
fillOpacity='0.4'
/>
<path
d='M100.04 111.354C101.742 113.056 101.571 115.873 99.5717 117.212C89.3989 124.026 77.1636 128 64 128C50.8364 128 38.6011 124.026 28.4283 117.212C26.4289 115.873 26.2581 113.056 27.9597 111.354L42.5755 96.7382C43.8965 95.4172 45.9457 95.2085 47.6084 96.0603C52.5248 98.579 58.0964 100 64 100C69.9036 100 75.4753 98.579 80.3916 96.0603C82.0543 95.2085 84.1036 95.4172 85.4245 96.7382L100.04 111.354Z'
fill='white'
/>
</svg>
)
}
export function MicrosoftIcon(props: SVGProps<SVGSVGElement>) {
return (
<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 23 23' {...props}>

View File

@@ -1,5 +1,16 @@
import type { LoopType, ParallelType } from '@/lib/workflows/types'
/**
* Runtime-injected keys for trigger blocks that should be hidden from logs/display.
* These are added during execution but aren't part of the block's static output schema.
*/
export const TRIGGER_INTERNAL_KEYS = ['webhook', 'workflowId'] as const
export type TriggerInternalKey = (typeof TRIGGER_INTERNAL_KEYS)[number]
export function isTriggerInternalKey(key: string): key is TriggerInternalKey {
return TRIGGER_INTERNAL_KEYS.includes(key as TriggerInternalKey)
}
export enum BlockType {
PARALLEL = 'parallel',
LOOP = 'loop',

View File

@@ -33,6 +33,15 @@ export interface DAG {
parallelConfigs: Map<string, SerializedParallel>
}
export interface DAGBuildOptions {
/** Trigger block ID to start path construction from */
triggerBlockId?: string
/** Saved incoming edges from snapshot for resumption */
savedIncomingEdges?: Record<string, string[]>
/** Include all enabled blocks instead of only those reachable from trigger */
includeAllBlocks?: boolean
}
export class DAGBuilder {
private pathConstructor = new PathConstructor()
private loopConstructor = new LoopConstructor()
@@ -40,11 +49,9 @@ export class DAGBuilder {
private nodeConstructor = new NodeConstructor()
private edgeConstructor = new EdgeConstructor()
build(
workflow: SerializedWorkflow,
triggerBlockId?: string,
savedIncomingEdges?: Record<string, string[]>
): DAG {
build(workflow: SerializedWorkflow, options: DAGBuildOptions = {}): DAG {
const { triggerBlockId, savedIncomingEdges, includeAllBlocks } = options
const dag: DAG = {
nodes: new Map(),
loopConfigs: new Map(),
@@ -53,7 +60,7 @@ export class DAGBuilder {
this.initializeConfigs(workflow, dag)
const reachableBlocks = this.pathConstructor.execute(workflow, triggerBlockId)
const reachableBlocks = this.pathConstructor.execute(workflow, triggerBlockId, includeAllBlocks)
this.loopConstructor.execute(dag, reachableBlocks)
this.parallelConstructor.execute(dag, reachableBlocks)

View File

@@ -6,7 +6,16 @@ import type { SerializedBlock, SerializedWorkflow } from '@/serializer/types'
const logger = createLogger('PathConstructor')
export class PathConstructor {
execute(workflow: SerializedWorkflow, triggerBlockId?: string): Set<string> {
execute(
workflow: SerializedWorkflow,
triggerBlockId?: string,
includeAllBlocks?: boolean
): Set<string> {
// For run-from-block mode, include all enabled blocks regardless of trigger reachability
if (includeAllBlocks) {
return this.getAllEnabledBlocks(workflow)
}
const resolvedTriggerId = this.findTriggerBlock(workflow, triggerBlockId)
if (!resolvedTriggerId) {

View File

@@ -11,8 +11,6 @@ import {
DEFAULTS,
EDGE,
isSentinelBlockType,
isTriggerBehavior,
isWorkflowBlockType,
} from '@/executor/constants'
import type { DAGNode } from '@/executor/dag/builder'
import { ChildWorkflowError } from '@/executor/errors/child-workflow-error'
@@ -30,6 +28,7 @@ import type {
} from '@/executor/types'
import { streamingResponseFormatProcessor } from '@/executor/utils'
import { buildBlockExecutionError, normalizeError } from '@/executor/utils/errors'
import { filterOutputForLog } from '@/executor/utils/output-filter'
import { validateBlockType } from '@/executor/utils/permission-check'
import type { VariableResolver } from '@/executor/variables/resolver'
import type { SerializedBlock } from '@/serializer/types'
@@ -149,13 +148,15 @@ export class BlockExecutor {
blockLog.endedAt = new Date().toISOString()
blockLog.durationMs = duration
blockLog.success = true
blockLog.output = this.filterOutputForLog(block, normalizedOutput)
blockLog.output = filterOutputForLog(block.metadata?.id || '', normalizedOutput, { block })
}
this.state.setBlockOutput(node.id, normalizedOutput, duration)
if (!isSentinel) {
const displayOutput = this.filterOutputForDisplay(block, normalizedOutput)
const displayOutput = filterOutputForLog(block.metadata?.id || '', normalizedOutput, {
block,
})
this.callOnBlockComplete(ctx, node, block, resolvedInputs, displayOutput, duration)
}
@@ -233,7 +234,7 @@ export class BlockExecutor {
blockLog.success = false
blockLog.error = errorMessage
blockLog.input = input
blockLog.output = this.filterOutputForLog(block, errorOutput)
blockLog.output = filterOutputForLog(block.metadata?.id || '', errorOutput, { block })
}
logger.error(
@@ -246,7 +247,7 @@ export class BlockExecutor {
)
if (!isSentinel) {
const displayOutput = this.filterOutputForDisplay(block, errorOutput)
const displayOutput = filterOutputForLog(block.metadata?.id || '', errorOutput, { block })
this.callOnBlockComplete(ctx, node, block, input, displayOutput, duration)
}
@@ -335,51 +336,6 @@ export class BlockExecutor {
return { result: output }
}
private filterOutputForLog(
block: SerializedBlock,
output: NormalizedBlockOutput
): NormalizedBlockOutput {
const blockType = block.metadata?.id
if (blockType === BlockType.HUMAN_IN_THE_LOOP) {
const filtered: NormalizedBlockOutput = {}
for (const [key, value] of Object.entries(output)) {
if (key.startsWith('_')) continue
if (key === 'response') continue
filtered[key] = value
}
return filtered
}
if (isTriggerBehavior(block)) {
const filtered: NormalizedBlockOutput = {}
const internalKeys = ['webhook', 'workflowId']
for (const [key, value] of Object.entries(output)) {
if (internalKeys.includes(key)) continue
filtered[key] = value
}
return filtered
}
return output
}
private filterOutputForDisplay(
block: SerializedBlock,
output: NormalizedBlockOutput
): NormalizedBlockOutput {
const filtered = this.filterOutputForLog(block, output)
if (isWorkflowBlockType(block.metadata?.id)) {
const { childTraceSpans: _, ...displayOutput } = filtered as {
childTraceSpans?: unknown
} & Record<string, unknown>
return displayOutput
}
return filtered
}
private callOnBlockStart(ctx: ExecutionContext, node: DAGNode, block: SerializedBlock): void {
const blockId = node.id
const blockName = block.metadata?.name ?? blockId

View File

@@ -26,6 +26,7 @@ export class ExecutionEngine {
private allowResumeTriggers: boolean
private cancelledFlag = false
private errorFlag = false
private stoppedEarlyFlag = false
private executionError: Error | null = null
private lastCancellationCheck = 0
private readonly useRedisCancellation: boolean
@@ -105,7 +106,7 @@ export class ExecutionEngine {
this.initializeQueue(triggerBlockId)
while (this.hasWork()) {
if ((await this.checkCancellation()) || this.errorFlag) {
if ((await this.checkCancellation()) || this.errorFlag || this.stoppedEarlyFlag) {
break
}
await this.processQueue()
@@ -259,6 +260,16 @@ export class ExecutionEngine {
}
private initializeQueue(triggerBlockId?: string): void {
if (this.context.runFromBlockContext) {
const { startBlockId } = this.context.runFromBlockContext
logger.info('Initializing queue for run-from-block mode', {
startBlockId,
dirtySetSize: this.context.runFromBlockContext.dirtySet.size,
})
this.addToQueue(startBlockId)
return
}
const pendingBlocks = this.context.metadata.pendingBlocks
const remainingEdges = (this.context.metadata as any).remainingEdges
@@ -385,6 +396,17 @@ export class ExecutionEngine {
this.finalOutput = output
}
if (this.context.stopAfterBlockId === nodeId) {
// For loop/parallel sentinels, only stop if the subflow has fully exited (all iterations done)
// shouldContinue: true means more iterations, shouldExit: true means loop is done
const shouldContinueLoop = output.shouldContinue === true
if (!shouldContinueLoop) {
logger.info('Stopping execution after target block', { nodeId })
this.stoppedEarlyFlag = true
return
}
}
const readyNodes = this.edgeManager.processOutgoingEdges(node, output, false)
logger.info('Processing outgoing edges', {

View File

@@ -5,17 +5,31 @@ import { BlockExecutor } from '@/executor/execution/block-executor'
import { EdgeManager } from '@/executor/execution/edge-manager'
import { ExecutionEngine } from '@/executor/execution/engine'
import { ExecutionState } from '@/executor/execution/state'
import type { ContextExtensions, WorkflowInput } from '@/executor/execution/types'
import type {
ContextExtensions,
SerializableExecutionState,
WorkflowInput,
} from '@/executor/execution/types'
import { createBlockHandlers } from '@/executor/handlers/registry'
import { LoopOrchestrator } from '@/executor/orchestrators/loop'
import { NodeExecutionOrchestrator } from '@/executor/orchestrators/node'
import { ParallelOrchestrator } from '@/executor/orchestrators/parallel'
import type { BlockState, ExecutionContext, ExecutionResult } from '@/executor/types'
import {
computeExecutionSets,
type RunFromBlockContext,
resolveContainerToSentinelStart,
validateRunFromBlock,
} from '@/executor/utils/run-from-block'
import {
buildResolutionFromBlock,
buildStartBlockOutput,
resolveExecutorStartBlock,
} from '@/executor/utils/start-block'
import {
extractLoopIdFromSentinel,
extractParallelIdFromSentinel,
} from '@/executor/utils/subflow-utils'
import { VariableResolver } from '@/executor/variables/resolver'
import type { SerializedWorkflow } from '@/serializer/types'
@@ -48,7 +62,10 @@ export class DAGExecutor {
async execute(workflowId: string, triggerBlockId?: string): Promise<ExecutionResult> {
const savedIncomingEdges = this.contextExtensions.dagIncomingEdges
const dag = this.dagBuilder.build(this.workflow, triggerBlockId, savedIncomingEdges)
const dag = this.dagBuilder.build(this.workflow, {
triggerBlockId,
savedIncomingEdges,
})
const { context, state } = this.createExecutionContext(workflowId, triggerBlockId)
const resolver = new VariableResolver(this.workflow, this.workflowVariables, state)
@@ -89,17 +106,151 @@ export class DAGExecutor {
}
}
/**
* Execute from a specific block using cached outputs for upstream blocks.
*/
async executeFromBlock(
workflowId: string,
startBlockId: string,
sourceSnapshot: SerializableExecutionState
): Promise<ExecutionResult> {
// Build full DAG with all blocks to compute upstream set for snapshot filtering
// includeAllBlocks is needed because the startBlockId might be a trigger not reachable from the main trigger
const dag = this.dagBuilder.build(this.workflow, { includeAllBlocks: true })
const executedBlocks = new Set(sourceSnapshot.executedBlocks)
const validation = validateRunFromBlock(startBlockId, dag, executedBlocks)
if (!validation.valid) {
throw new Error(validation.error)
}
const { dirtySet, upstreamSet } = computeExecutionSets(dag, startBlockId)
const effectiveStartBlockId = resolveContainerToSentinelStart(startBlockId, dag) ?? startBlockId
// Extract container IDs from sentinel IDs in upstream set
const upstreamContainerIds = new Set<string>()
for (const nodeId of upstreamSet) {
const loopId = extractLoopIdFromSentinel(nodeId)
if (loopId) upstreamContainerIds.add(loopId)
const parallelId = extractParallelIdFromSentinel(nodeId)
if (parallelId) upstreamContainerIds.add(parallelId)
}
// Filter snapshot to only include upstream blocks - prevents references to non-upstream blocks
const filteredBlockStates: Record<string, any> = {}
for (const [blockId, state] of Object.entries(sourceSnapshot.blockStates)) {
if (upstreamSet.has(blockId) || upstreamContainerIds.has(blockId)) {
filteredBlockStates[blockId] = state
}
}
const filteredExecutedBlocks = sourceSnapshot.executedBlocks.filter(
(id) => upstreamSet.has(id) || upstreamContainerIds.has(id)
)
// Filter loop/parallel executions to only include upstream containers
const filteredLoopExecutions: Record<string, any> = {}
if (sourceSnapshot.loopExecutions) {
for (const [loopId, execution] of Object.entries(sourceSnapshot.loopExecutions)) {
if (upstreamContainerIds.has(loopId)) {
filteredLoopExecutions[loopId] = execution
}
}
}
const filteredParallelExecutions: Record<string, any> = {}
if (sourceSnapshot.parallelExecutions) {
for (const [parallelId, execution] of Object.entries(sourceSnapshot.parallelExecutions)) {
if (upstreamContainerIds.has(parallelId)) {
filteredParallelExecutions[parallelId] = execution
}
}
}
const filteredSnapshot: SerializableExecutionState = {
...sourceSnapshot,
blockStates: filteredBlockStates,
executedBlocks: filteredExecutedBlocks,
loopExecutions: filteredLoopExecutions,
parallelExecutions: filteredParallelExecutions,
}
logger.info('Executing from block', {
workflowId,
startBlockId,
effectiveStartBlockId,
dirtySetSize: dirtySet.size,
upstreamSetSize: upstreamSet.size,
})
// Remove incoming edges from non-dirty sources so convergent blocks don't wait for cached upstream
for (const nodeId of dirtySet) {
const node = dag.nodes.get(nodeId)
if (!node) continue
const nonDirtyIncoming: string[] = []
for (const sourceId of node.incomingEdges) {
if (!dirtySet.has(sourceId)) {
nonDirtyIncoming.push(sourceId)
}
}
for (const sourceId of nonDirtyIncoming) {
node.incomingEdges.delete(sourceId)
}
}
const runFromBlockContext = { startBlockId: effectiveStartBlockId, dirtySet }
const { context, state } = this.createExecutionContext(workflowId, undefined, {
snapshotState: filteredSnapshot,
runFromBlockContext,
})
const resolver = new VariableResolver(this.workflow, this.workflowVariables, state)
const loopOrchestrator = new LoopOrchestrator(dag, state, resolver)
loopOrchestrator.setContextExtensions(this.contextExtensions)
const parallelOrchestrator = new ParallelOrchestrator(dag, state)
parallelOrchestrator.setResolver(resolver)
parallelOrchestrator.setContextExtensions(this.contextExtensions)
const allHandlers = createBlockHandlers()
const blockExecutor = new BlockExecutor(allHandlers, resolver, this.contextExtensions, state)
const edgeManager = new EdgeManager(dag)
loopOrchestrator.setEdgeManager(edgeManager)
const nodeOrchestrator = new NodeExecutionOrchestrator(
dag,
state,
blockExecutor,
loopOrchestrator,
parallelOrchestrator
)
const engine = new ExecutionEngine(context, dag, edgeManager, nodeOrchestrator)
return await engine.run()
}
private createExecutionContext(
workflowId: string,
triggerBlockId?: string
triggerBlockId?: string,
overrides?: {
snapshotState?: SerializableExecutionState
runFromBlockContext?: RunFromBlockContext
}
): { context: ExecutionContext; state: ExecutionState } {
const snapshotState = this.contextExtensions.snapshotState
const snapshotState = overrides?.snapshotState ?? this.contextExtensions.snapshotState
const blockStates = snapshotState?.blockStates
? new Map(Object.entries(snapshotState.blockStates))
: new Map<string, BlockState>()
const executedBlocks = snapshotState?.executedBlocks
let executedBlocks = snapshotState?.executedBlocks
? new Set(snapshotState.executedBlocks)
: new Set<string>()
if (overrides?.runFromBlockContext) {
const { dirtySet } = overrides.runFromBlockContext
executedBlocks = new Set([...executedBlocks].filter((id) => !dirtySet.has(id)))
logger.info('Cleared executed status for dirty blocks', {
dirtySetSize: dirtySet.size,
remainingExecutedBlocks: executedBlocks.size,
})
}
const state = new ExecutionState(blockStates, executedBlocks)
const context: ExecutionContext = {
@@ -109,7 +260,7 @@ export class DAGExecutor {
userId: this.contextExtensions.userId,
isDeployedContext: this.contextExtensions.isDeployedContext,
blockStates: state.getBlockStates(),
blockLogs: snapshotState?.blockLogs ?? [],
blockLogs: overrides?.runFromBlockContext ? [] : (snapshotState?.blockLogs ?? []),
metadata: {
...this.contextExtensions.metadata,
startTime: new Date().toISOString(),
@@ -169,6 +320,8 @@ export class DAGExecutor {
abortSignal: this.contextExtensions.abortSignal,
includeFileBase64: this.contextExtensions.includeFileBase64,
base64MaxBytes: this.contextExtensions.base64MaxBytes,
runFromBlockContext: overrides?.runFromBlockContext,
stopAfterBlockId: this.contextExtensions.stopAfterBlockId,
}
if (this.contextExtensions.resumeFromSnapshot) {
@@ -193,6 +346,15 @@ export class DAGExecutor {
pendingBlocks: context.metadata.pendingBlocks,
skipStarterBlockInit: true,
})
} else if (overrides?.runFromBlockContext) {
// In run-from-block mode, initialize the start block only if it's a regular block
// Skip for sentinels/containers (loop/parallel) which aren't real blocks
const startBlockId = overrides.runFromBlockContext.startBlockId
const isRegularBlock = this.workflow.blocks.some((b) => b.id === startBlockId)
if (isRegularBlock) {
this.initializeStarterBlock(context, state, startBlockId)
}
} else {
this.initializeStarterBlock(context, state, triggerBlockId)
}

View File

@@ -1,5 +1,6 @@
import type { Edge } from 'reactflow'
import type { BlockLog, BlockState, NormalizedBlockOutput } from '@/executor/types'
import type { RunFromBlockContext } from '@/executor/utils/run-from-block'
import type { SubflowType } from '@/stores/workflows/workflow/types'
export interface ExecutionMetadata {
@@ -105,6 +106,17 @@ export interface ContextExtensions {
output: { input?: any; output: NormalizedBlockOutput; executionTime: number },
iterationContext?: IterationContext
) => Promise<void>
/**
* Run-from-block configuration. When provided, executor runs in partial
* execution mode starting from the specified block.
*/
runFromBlockContext?: RunFromBlockContext
/**
* Stop execution after this block completes. Used for "run until block" feature.
*/
stopAfterBlockId?: string
}
export interface WorkflowInput {

View File

@@ -1,5 +1,5 @@
import { createLogger } from '@sim/logger'
import { BlockType, isTriggerBehavior } from '@/executor/constants'
import { BlockType, isTriggerBehavior, isTriggerInternalKey } from '@/executor/constants'
import type { BlockHandler, ExecutionContext } from '@/executor/types'
import type { SerializedBlock } from '@/serializer/types'
@@ -33,7 +33,12 @@ export class TriggerBlockHandler implements BlockHandler {
const starterOutput = starterState.output
if (starterOutput.webhook?.data) {
const { webhook, workflowId, ...cleanOutput } = starterOutput
const cleanOutput: Record<string, unknown> = {}
for (const [key, value] of Object.entries(starterOutput)) {
if (!isTriggerInternalKey(key)) {
cleanOutput[key] = value
}
}
return cleanOutput
}

View File

@@ -276,7 +276,16 @@ export class LoopOrchestrator {
scope: LoopScope
): LoopContinuationResult {
const results = scope.allIterationOutputs
this.state.setBlockOutput(loopId, { results }, DEFAULTS.EXECUTION_TIME)
const output = { results }
this.state.setBlockOutput(loopId, output, DEFAULTS.EXECUTION_TIME)
// Emit onBlockComplete for the loop container so the UI can track it
if (this.contextExtensions?.onBlockComplete) {
this.contextExtensions.onBlockComplete(loopId, 'Loop', 'loop', {
output,
executionTime: DEFAULTS.EXECUTION_TIME,
})
}
return {
shouldContinue: false,

View File

@@ -31,7 +31,18 @@ export class NodeExecutionOrchestrator {
throw new Error(`Node not found in DAG: ${nodeId}`)
}
if (this.state.hasExecuted(nodeId)) {
if (ctx.runFromBlockContext && !ctx.runFromBlockContext.dirtySet.has(nodeId)) {
const cachedOutput = this.state.getBlockOutput(nodeId) || {}
logger.debug('Skipping non-dirty block in run-from-block mode', { nodeId })
return {
nodeId,
output: cachedOutput,
isFinalOutput: false,
}
}
const isDirtyBlock = ctx.runFromBlockContext?.dirtySet.has(nodeId) ?? false
if (!isDirtyBlock && this.state.hasExecuted(nodeId)) {
const output = this.state.getBlockOutput(nodeId) || {}
return {
nodeId,

View File

@@ -228,9 +228,17 @@ export class ParallelOrchestrator {
const branchOutputs = scope.branchOutputs.get(i) || []
results.push(branchOutputs)
}
this.state.setBlockOutput(parallelId, {
results,
})
const output = { results }
this.state.setBlockOutput(parallelId, output)
// Emit onBlockComplete for the parallel container so the UI can track it
if (this.contextExtensions?.onBlockComplete) {
this.contextExtensions.onBlockComplete(parallelId, 'Parallel', 'parallel', {
output,
executionTime: 0,
})
}
return {
allBranchesComplete: true,
results,

View File

@@ -1,6 +1,7 @@
import type { TraceSpan } from '@/lib/logs/types'
import type { PermissionGroupConfig } from '@/lib/permission-groups/types'
import type { BlockOutput } from '@/blocks/types'
import type { RunFromBlockContext } from '@/executor/utils/run-from-block'
import type { SerializedBlock, SerializedWorkflow } from '@/serializer/types'
export interface UserFile {
@@ -250,6 +251,17 @@ export interface ExecutionContext {
* will not have their base64 content fetched.
*/
base64MaxBytes?: number
/**
* Context for "run from block" mode. When present, only blocks in dirtySet
* will be executed; others return cached outputs from the source snapshot.
*/
runFromBlockContext?: RunFromBlockContext
/**
* Stop execution after this block completes. Used for "run until block" feature.
*/
stopAfterBlockId?: string
}
export interface ExecutionResult {

View File

@@ -0,0 +1,51 @@
import { getBlock } from '@/blocks'
import { isHiddenFromDisplay } from '@/blocks/types'
import { isTriggerBehavior, isTriggerInternalKey } from '@/executor/constants'
import type { NormalizedBlockOutput } from '@/executor/types'
import type { SerializedBlock } from '@/serializer/types'
/**
* Filters block output for logging/display purposes.
* Removes internal fields and fields marked with hiddenFromDisplay.
*
* @param blockType - The block type string (e.g., 'human_in_the_loop', 'workflow')
* @param output - The raw block output to filter
* @param options - Optional configuration
* @param options.block - Full SerializedBlock for trigger behavior detection
* @param options.additionalHiddenKeys - Extra keys to filter out (e.g., 'resume')
*/
export function filterOutputForLog(
blockType: string,
output: NormalizedBlockOutput,
options?: {
block?: SerializedBlock
additionalHiddenKeys?: string[]
}
): NormalizedBlockOutput {
const blockConfig = blockType ? getBlock(blockType) : undefined
const filtered: NormalizedBlockOutput = {}
const additionalHiddenKeys = options?.additionalHiddenKeys ?? []
for (const [key, value] of Object.entries(output)) {
// Skip internal keys (underscore prefix)
if (key.startsWith('_')) continue
if (blockConfig?.outputs && isHiddenFromDisplay(blockConfig.outputs[key])) {
continue
}
// Skip runtime-injected trigger keys not in block config
if (options?.block && isTriggerBehavior(options.block) && isTriggerInternalKey(key)) {
continue
}
// Skip additional hidden keys specified by caller
if (additionalHiddenKeys.includes(key)) {
continue
}
filtered[key] = value
}
return filtered
}

View File

@@ -0,0 +1,610 @@
import { describe, expect, it } from 'vitest'
import type { DAG, DAGNode } from '@/executor/dag/builder'
import type { DAGEdge, NodeMetadata } from '@/executor/dag/types'
import { computeExecutionSets, validateRunFromBlock } from '@/executor/utils/run-from-block'
import type { SerializedLoop, SerializedParallel } from '@/serializer/types'
/**
* Helper to extract dirty set from computeExecutionSets
*/
function computeDirtySet(dag: DAG, startBlockId: string): Set<string> {
return computeExecutionSets(dag, startBlockId).dirtySet
}
/**
* Helper to create a DAG node for testing
*/
function createNode(
id: string,
outgoingEdges: Array<{ target: string; sourceHandle?: string }> = [],
metadata: Partial<NodeMetadata> = {}
): DAGNode {
const edges = new Map<string, DAGEdge>()
for (const edge of outgoingEdges) {
edges.set(edge.target, { target: edge.target, sourceHandle: edge.sourceHandle })
}
return {
id,
block: {
id,
position: { x: 0, y: 0 },
config: { tool: 'test', params: {} },
inputs: {},
outputs: {},
metadata: { id: 'test', name: `block-${id}`, category: 'tools' },
enabled: true,
},
incomingEdges: new Set<string>(),
outgoingEdges: edges,
metadata: {
isParallelBranch: false,
isLoopNode: false,
isSentinel: false,
...metadata,
},
}
}
/**
* Helper to create a DAG for testing
*/
function createDAG(nodes: DAGNode[]): DAG {
const nodeMap = new Map<string, DAGNode>()
for (const node of nodes) {
nodeMap.set(node.id, node)
}
// Set up incoming edges based on outgoing edges
for (const node of nodes) {
for (const [, edge] of node.outgoingEdges) {
const targetNode = nodeMap.get(edge.target)
if (targetNode) {
targetNode.incomingEdges.add(node.id)
}
}
}
return {
nodes: nodeMap,
loopConfigs: new Map<string, SerializedLoop>(),
parallelConfigs: new Map<string, SerializedParallel>(),
}
}
describe('computeDirtySet', () => {
it('includes start block in dirty set', () => {
const dag = createDAG([createNode('A'), createNode('B'), createNode('C')])
const dirtySet = computeDirtySet(dag, 'B')
expect(dirtySet.has('B')).toBe(true)
})
it('includes all downstream blocks in linear workflow', () => {
// A → B → C → D
const dag = createDAG([
createNode('A', [{ target: 'B' }]),
createNode('B', [{ target: 'C' }]),
createNode('C', [{ target: 'D' }]),
createNode('D'),
])
const dirtySet = computeDirtySet(dag, 'B')
expect(dirtySet.has('A')).toBe(false)
expect(dirtySet.has('B')).toBe(true)
expect(dirtySet.has('C')).toBe(true)
expect(dirtySet.has('D')).toBe(true)
expect(dirtySet.size).toBe(3)
})
it('handles branching paths', () => {
// A → B → C
// ↓
// D → E
const dag = createDAG([
createNode('A', [{ target: 'B' }]),
createNode('B', [{ target: 'C' }, { target: 'D' }]),
createNode('C'),
createNode('D', [{ target: 'E' }]),
createNode('E'),
])
const dirtySet = computeDirtySet(dag, 'B')
expect(dirtySet.has('A')).toBe(false)
expect(dirtySet.has('B')).toBe(true)
expect(dirtySet.has('C')).toBe(true)
expect(dirtySet.has('D')).toBe(true)
expect(dirtySet.has('E')).toBe(true)
expect(dirtySet.size).toBe(4)
})
it('handles convergence points', () => {
// A → C
// B → C → D
const dag = createDAG([
createNode('A', [{ target: 'C' }]),
createNode('B', [{ target: 'C' }]),
createNode('C', [{ target: 'D' }]),
createNode('D'),
])
// Run from A: should include A, C, D (but not B)
const dirtySet = computeDirtySet(dag, 'A')
expect(dirtySet.has('A')).toBe(true)
expect(dirtySet.has('B')).toBe(false)
expect(dirtySet.has('C')).toBe(true)
expect(dirtySet.has('D')).toBe(true)
expect(dirtySet.size).toBe(3)
})
it('handles diamond pattern', () => {
// B
// ↗ ↘
// A D
// ↘ ↗
// C
const dag = createDAG([
createNode('A', [{ target: 'B' }, { target: 'C' }]),
createNode('B', [{ target: 'D' }]),
createNode('C', [{ target: 'D' }]),
createNode('D'),
])
const dirtySet = computeDirtySet(dag, 'A')
expect(dirtySet.has('A')).toBe(true)
expect(dirtySet.has('B')).toBe(true)
expect(dirtySet.has('C')).toBe(true)
expect(dirtySet.has('D')).toBe(true)
expect(dirtySet.size).toBe(4)
})
it('stops at graph boundaries', () => {
// A → B C → D (disconnected)
const dag = createDAG([
createNode('A', [{ target: 'B' }]),
createNode('B'),
createNode('C', [{ target: 'D' }]),
createNode('D'),
])
const dirtySet = computeDirtySet(dag, 'A')
expect(dirtySet.has('A')).toBe(true)
expect(dirtySet.has('B')).toBe(true)
expect(dirtySet.has('C')).toBe(false)
expect(dirtySet.has('D')).toBe(false)
expect(dirtySet.size).toBe(2)
})
it('handles single node workflow', () => {
const dag = createDAG([createNode('A')])
const dirtySet = computeDirtySet(dag, 'A')
expect(dirtySet.has('A')).toBe(true)
expect(dirtySet.size).toBe(1)
})
it('handles node not in DAG gracefully', () => {
const dag = createDAG([createNode('A'), createNode('B')])
const dirtySet = computeDirtySet(dag, 'nonexistent')
// Should just contain the start block ID even if not found
expect(dirtySet.has('nonexistent')).toBe(true)
expect(dirtySet.size).toBe(1)
})
it('includes convergent block when running from one branch of parallel', () => {
// Parallel branches converging:
// A → B → D
// A → C → D
// Running from B should include B and D (but not A or C)
const dag = createDAG([
createNode('A', [{ target: 'B' }, { target: 'C' }]),
createNode('B', [{ target: 'D' }]),
createNode('C', [{ target: 'D' }]),
createNode('D'),
])
const dirtySet = computeDirtySet(dag, 'B')
expect(dirtySet.has('A')).toBe(false)
expect(dirtySet.has('B')).toBe(true)
expect(dirtySet.has('C')).toBe(false)
expect(dirtySet.has('D')).toBe(true)
expect(dirtySet.size).toBe(2)
})
it('handles running from convergent block itself (all upstream non-dirty)', () => {
// A → C
// B → C
// Running from C should only include C
const dag = createDAG([
createNode('A', [{ target: 'C' }]),
createNode('B', [{ target: 'C' }]),
createNode('C', [{ target: 'D' }]),
createNode('D'),
])
const dirtySet = computeDirtySet(dag, 'C')
expect(dirtySet.has('A')).toBe(false)
expect(dirtySet.has('B')).toBe(false)
expect(dirtySet.has('C')).toBe(true)
expect(dirtySet.has('D')).toBe(true)
expect(dirtySet.size).toBe(2)
})
it('handles deep downstream chains', () => {
// A → B → C → D → E → F
// Running from C should include C, D, E, F
const dag = createDAG([
createNode('A', [{ target: 'B' }]),
createNode('B', [{ target: 'C' }]),
createNode('C', [{ target: 'D' }]),
createNode('D', [{ target: 'E' }]),
createNode('E', [{ target: 'F' }]),
createNode('F'),
])
const dirtySet = computeDirtySet(dag, 'C')
expect(dirtySet.has('A')).toBe(false)
expect(dirtySet.has('B')).toBe(false)
expect(dirtySet.has('C')).toBe(true)
expect(dirtySet.has('D')).toBe(true)
expect(dirtySet.has('E')).toBe(true)
expect(dirtySet.has('F')).toBe(true)
expect(dirtySet.size).toBe(4)
})
})
describe('validateRunFromBlock', () => {
it('accepts valid block', () => {
const dag = createDAG([createNode('A'), createNode('B')])
const executedBlocks = new Set(['A', 'B'])
const result = validateRunFromBlock('A', dag, executedBlocks)
expect(result.valid).toBe(true)
expect(result.error).toBeUndefined()
})
it('rejects block not found in DAG', () => {
const dag = createDAG([createNode('A')])
const executedBlocks = new Set(['A', 'B'])
const result = validateRunFromBlock('B', dag, executedBlocks)
expect(result.valid).toBe(false)
expect(result.error).toContain('Block not found')
})
it('rejects blocks inside loops', () => {
const dag = createDAG([createNode('A', [], { isLoopNode: true, loopId: 'loop-1' })])
const executedBlocks = new Set(['A'])
const result = validateRunFromBlock('A', dag, executedBlocks)
expect(result.valid).toBe(false)
expect(result.error).toContain('inside loop')
expect(result.error).toContain('loop-1')
})
it('rejects blocks inside parallels', () => {
const dag = createDAG([
createNode('A', [], { isParallelBranch: true, parallelId: 'parallel-1' }),
])
const executedBlocks = new Set(['A'])
const result = validateRunFromBlock('A', dag, executedBlocks)
expect(result.valid).toBe(false)
expect(result.error).toContain('inside parallel')
expect(result.error).toContain('parallel-1')
})
it('rejects sentinel nodes', () => {
const dag = createDAG([createNode('A', [], { isSentinel: true, sentinelType: 'start' })])
const executedBlocks = new Set(['A'])
const result = validateRunFromBlock('A', dag, executedBlocks)
expect(result.valid).toBe(false)
expect(result.error).toContain('sentinel')
})
it('rejects blocks with unexecuted upstream dependencies', () => {
// A → B, only A executed but B depends on A
const dag = createDAG([createNode('A', [{ target: 'B' }]), createNode('B')])
const executedBlocks = new Set<string>() // A was not executed
const result = validateRunFromBlock('B', dag, executedBlocks)
expect(result.valid).toBe(false)
expect(result.error).toContain('Upstream dependency not executed')
})
it('allows running from block when immediate predecessor was executed (ignores transitive)', () => {
// A → X → B → C, where X is new (not executed)
// Running from C is allowed because B (immediate predecessor) was executed
// C will use B's cached output - doesn't matter that X is new
const dag = createDAG([
createNode('A', [{ target: 'X' }]),
createNode('X', [{ target: 'B' }]),
createNode('B', [{ target: 'C' }]),
createNode('C'),
])
const executedBlocks = new Set(['A', 'B', 'C']) // X was not executed (new block)
const result = validateRunFromBlock('C', dag, executedBlocks)
// Valid because C's immediate predecessor B was executed
expect(result.valid).toBe(true)
})
it('allows blocks with no dependencies even if not previously executed', () => {
// A and B are independent (no edges)
const dag = createDAG([createNode('A'), createNode('B')])
const executedBlocks = new Set(['A']) // B was not executed but has no deps
const result = validateRunFromBlock('B', dag, executedBlocks)
expect(result.valid).toBe(true) // B has no incoming edges, so it's valid
})
it('accepts regular executed block', () => {
const dag = createDAG([
createNode('trigger', [{ target: 'A' }]),
createNode('A', [{ target: 'B' }]),
createNode('B'),
])
const executedBlocks = new Set(['trigger', 'A', 'B'])
const result = validateRunFromBlock('A', dag, executedBlocks)
expect(result.valid).toBe(true)
})
it('accepts loop container when executed', () => {
// Loop container with sentinel nodes
const loopId = 'loop-container-1'
const sentinelStartId = `loop-${loopId}-sentinel-start`
const sentinelEndId = `loop-${loopId}-sentinel-end`
const dag = createDAG([
createNode('A', [{ target: sentinelStartId }]),
createNode(sentinelStartId, [{ target: 'B' }], {
isSentinel: true,
sentinelType: 'start',
loopId,
}),
createNode('B', [{ target: sentinelEndId }], { isLoopNode: true, loopId }),
createNode(sentinelEndId, [{ target: 'C' }], {
isSentinel: true,
sentinelType: 'end',
loopId,
}),
createNode('C'),
])
dag.loopConfigs.set(loopId, { id: loopId, nodes: ['B'], iterations: 3, loopType: 'for' } as any)
const executedBlocks = new Set(['A', loopId, sentinelStartId, 'B', sentinelEndId, 'C'])
const result = validateRunFromBlock(loopId, dag, executedBlocks)
expect(result.valid).toBe(true)
})
it('accepts parallel container when executed', () => {
// Parallel container with sentinel nodes
const parallelId = 'parallel-container-1'
const sentinelStartId = `parallel-${parallelId}-sentinel-start`
const sentinelEndId = `parallel-${parallelId}-sentinel-end`
const dag = createDAG([
createNode('A', [{ target: sentinelStartId }]),
createNode(sentinelStartId, [{ target: 'B₍0₎' }], {
isSentinel: true,
sentinelType: 'start',
parallelId,
}),
createNode('B₍0₎', [{ target: sentinelEndId }], { isParallelBranch: true, parallelId }),
createNode(sentinelEndId, [{ target: 'C' }], {
isSentinel: true,
sentinelType: 'end',
parallelId,
}),
createNode('C'),
])
dag.parallelConfigs.set(parallelId, { id: parallelId, nodes: ['B'], count: 2 } as any)
const executedBlocks = new Set(['A', parallelId, sentinelStartId, 'B₍0₎', sentinelEndId, 'C'])
const result = validateRunFromBlock(parallelId, dag, executedBlocks)
expect(result.valid).toBe(true)
})
it('allows loop container with no upstream dependencies', () => {
// Loop containers are validated via their sentinel nodes, not incoming edges on the container itself
// If the loop has no upstream dependencies, it should be valid
const loopId = 'loop-container-1'
const sentinelStartId = `loop-${loopId}-sentinel-start`
const dag = createDAG([
createNode(sentinelStartId, [], { isSentinel: true, sentinelType: 'start', loopId }),
])
dag.loopConfigs.set(loopId, { id: loopId, nodes: [], iterations: 3, loopType: 'for' } as any)
const executedBlocks = new Set<string>() // Nothing executed but loop has no deps
const result = validateRunFromBlock(loopId, dag, executedBlocks)
// Loop container validation doesn't check incoming edges (containers don't have nodes in dag.nodes)
// So this is valid - the loop can start fresh
expect(result.valid).toBe(true)
})
})
describe('computeDirtySet with containers', () => {
it('includes loop container and all downstream when running from loop', () => {
// A → loop-sentinel-start → B (inside loop) → loop-sentinel-end → C
const loopId = 'loop-1'
const sentinelStartId = `loop-${loopId}-sentinel-start`
const sentinelEndId = `loop-${loopId}-sentinel-end`
const dag = createDAG([
createNode('A', [{ target: sentinelStartId }]),
createNode(sentinelStartId, [{ target: 'B' }], {
isSentinel: true,
sentinelType: 'start',
loopId,
}),
createNode('B', [{ target: sentinelEndId }], { isLoopNode: true, loopId }),
createNode(sentinelEndId, [{ target: 'C' }], {
isSentinel: true,
sentinelType: 'end',
loopId,
}),
createNode('C'),
])
dag.loopConfigs.set(loopId, { id: loopId, nodes: ['B'], iterations: 3, loopType: 'for' } as any)
const dirtySet = computeDirtySet(dag, loopId)
// Should include loop container, sentinel-start, B, sentinel-end, C
expect(dirtySet.has(loopId)).toBe(true)
expect(dirtySet.has(sentinelStartId)).toBe(true)
expect(dirtySet.has('B')).toBe(true)
expect(dirtySet.has(sentinelEndId)).toBe(true)
expect(dirtySet.has('C')).toBe(true)
// Should NOT include A (upstream)
expect(dirtySet.has('A')).toBe(false)
})
it('includes parallel container and all downstream when running from parallel', () => {
// A → parallel-sentinel-start → B₍0₎ → parallel-sentinel-end → C
const parallelId = 'parallel-1'
const sentinelStartId = `parallel-${parallelId}-sentinel-start`
const sentinelEndId = `parallel-${parallelId}-sentinel-end`
const dag = createDAG([
createNode('A', [{ target: sentinelStartId }]),
createNode(sentinelStartId, [{ target: 'B₍0₎' }], {
isSentinel: true,
sentinelType: 'start',
parallelId,
}),
createNode('B₍0₎', [{ target: sentinelEndId }], { isParallelBranch: true, parallelId }),
createNode(sentinelEndId, [{ target: 'C' }], {
isSentinel: true,
sentinelType: 'end',
parallelId,
}),
createNode('C'),
])
dag.parallelConfigs.set(parallelId, { id: parallelId, nodes: ['B'], count: 2 } as any)
const dirtySet = computeDirtySet(dag, parallelId)
// Should include parallel container, sentinel-start, B₍0₎, sentinel-end, C
expect(dirtySet.has(parallelId)).toBe(true)
expect(dirtySet.has(sentinelStartId)).toBe(true)
expect(dirtySet.has('B₍0₎')).toBe(true)
expect(dirtySet.has(sentinelEndId)).toBe(true)
expect(dirtySet.has('C')).toBe(true)
// Should NOT include A (upstream)
expect(dirtySet.has('A')).toBe(false)
})
})
describe('computeExecutionSets upstream set', () => {
it('includes all upstream blocks in linear workflow', () => {
// A → B → C → D
const dag = createDAG([
createNode('A', [{ target: 'B' }]),
createNode('B', [{ target: 'C' }]),
createNode('C', [{ target: 'D' }]),
createNode('D'),
])
const { upstreamSet } = computeExecutionSets(dag, 'C')
expect(upstreamSet.has('A')).toBe(true)
expect(upstreamSet.has('B')).toBe(true)
expect(upstreamSet.has('C')).toBe(false) // start block not in upstream
expect(upstreamSet.has('D')).toBe(false) // downstream
})
it('includes all branches in convergent upstream', () => {
// A → C
// B → C → D
const dag = createDAG([
createNode('A', [{ target: 'C' }]),
createNode('B', [{ target: 'C' }]),
createNode('C', [{ target: 'D' }]),
createNode('D'),
])
const { upstreamSet } = computeExecutionSets(dag, 'C')
expect(upstreamSet.has('A')).toBe(true)
expect(upstreamSet.has('B')).toBe(true)
expect(upstreamSet.has('C')).toBe(false)
expect(upstreamSet.has('D')).toBe(false)
})
it('excludes parallel branches not in upstream path', () => {
// A → B → D
// A → C → D
// Running from B: upstream is A only, not C
const dag = createDAG([
createNode('A', [{ target: 'B' }, { target: 'C' }]),
createNode('B', [{ target: 'D' }]),
createNode('C', [{ target: 'D' }]),
createNode('D'),
])
const { upstreamSet, dirtySet } = computeExecutionSets(dag, 'B')
// Upstream should only contain A
expect(upstreamSet.has('A')).toBe(true)
expect(upstreamSet.has('C')).toBe(false) // parallel branch, not upstream of B
// Dirty should contain B and D
expect(dirtySet.has('B')).toBe(true)
expect(dirtySet.has('D')).toBe(true)
expect(dirtySet.has('C')).toBe(false)
})
it('handles diamond pattern upstream correctly', () => {
// B
// ↗ ↘
// A D → E
// ↘ ↗
// C
// Running from D: upstream should be A, B, C
const dag = createDAG([
createNode('A', [{ target: 'B' }, { target: 'C' }]),
createNode('B', [{ target: 'D' }]),
createNode('C', [{ target: 'D' }]),
createNode('D', [{ target: 'E' }]),
createNode('E'),
])
const { upstreamSet, dirtySet } = computeExecutionSets(dag, 'D')
expect(upstreamSet.has('A')).toBe(true)
expect(upstreamSet.has('B')).toBe(true)
expect(upstreamSet.has('C')).toBe(true)
expect(upstreamSet.has('D')).toBe(false)
expect(dirtySet.has('D')).toBe(true)
expect(dirtySet.has('E')).toBe(true)
})
it('returns empty upstream set for root block', () => {
const dag = createDAG([createNode('A', [{ target: 'B' }]), createNode('B')])
const { upstreamSet } = computeExecutionSets(dag, 'A')
expect(upstreamSet.size).toBe(0)
})
})

View File

@@ -0,0 +1,193 @@
import { LOOP, PARALLEL } from '@/executor/constants'
import type { DAG } from '@/executor/dag/builder'
/**
* Builds the sentinel-start node ID for a loop.
*/
function buildLoopSentinelStartId(loopId: string): string {
return `${LOOP.SENTINEL.PREFIX}${loopId}${LOOP.SENTINEL.START_SUFFIX}`
}
/**
* Builds the sentinel-start node ID for a parallel.
*/
function buildParallelSentinelStartId(parallelId: string): string {
return `${PARALLEL.SENTINEL.PREFIX}${parallelId}${PARALLEL.SENTINEL.START_SUFFIX}`
}
/**
* Checks if a block ID is a loop or parallel container and returns the sentinel-start ID if so.
* Returns null if the block is not a container.
*/
export function resolveContainerToSentinelStart(blockId: string, dag: DAG): string | null {
if (dag.loopConfigs.has(blockId)) {
return buildLoopSentinelStartId(blockId)
}
if (dag.parallelConfigs.has(blockId)) {
return buildParallelSentinelStartId(blockId)
}
return null
}
/**
* Result of validating a block for run-from-block execution.
*/
export interface RunFromBlockValidation {
valid: boolean
error?: string
}
/**
* Context for run-from-block execution mode.
*/
export interface RunFromBlockContext {
/** The block ID to start execution from */
startBlockId: string
/** Set of block IDs that need re-execution (start block + all downstream) */
dirtySet: Set<string>
}
/**
* Result of computing execution sets for run-from-block mode.
*/
export interface ExecutionSets {
/** Blocks that need re-execution (start block + all downstream) */
dirtySet: Set<string>
/** Blocks that are upstream (ancestors) of the start block */
upstreamSet: Set<string>
}
/**
* Computes both the dirty set (downstream) and upstream set in a single traversal pass.
* - Dirty set: start block + all blocks reachable via outgoing edges (need re-execution)
* - Upstream set: all blocks reachable via incoming edges (can be referenced)
*
* For loop/parallel containers, starts from the sentinel-start node and includes
* the container ID itself in the dirty set.
*
* @param dag - The workflow DAG
* @param startBlockId - The block to start execution from
* @returns Object containing both dirtySet and upstreamSet
*/
export function computeExecutionSets(dag: DAG, startBlockId: string): ExecutionSets {
const dirty = new Set<string>([startBlockId])
const upstream = new Set<string>()
const sentinelStartId = resolveContainerToSentinelStart(startBlockId, dag)
const traversalStartId = sentinelStartId ?? startBlockId
if (sentinelStartId) {
dirty.add(sentinelStartId)
}
// BFS downstream for dirty set
const downstreamQueue = [traversalStartId]
while (downstreamQueue.length > 0) {
const nodeId = downstreamQueue.shift()!
const node = dag.nodes.get(nodeId)
if (!node) continue
for (const [, edge] of node.outgoingEdges) {
if (!dirty.has(edge.target)) {
dirty.add(edge.target)
downstreamQueue.push(edge.target)
}
}
}
// BFS upstream for upstream set
const upstreamQueue = [traversalStartId]
while (upstreamQueue.length > 0) {
const nodeId = upstreamQueue.shift()!
const node = dag.nodes.get(nodeId)
if (!node) continue
for (const sourceId of node.incomingEdges) {
if (!upstream.has(sourceId)) {
upstream.add(sourceId)
upstreamQueue.push(sourceId)
}
}
}
return { dirtySet: dirty, upstreamSet: upstream }
}
/**
* Validates that a block can be used as a run-from-block starting point.
*
* Validation rules:
* - Block must exist in the DAG (or be a loop/parallel container)
* - Block cannot be inside a loop (but loop containers are allowed)
* - Block cannot be inside a parallel (but parallel containers are allowed)
* - Block cannot be a sentinel node
* - All upstream dependencies must have been executed (have cached outputs)
*
* @param blockId - The block ID to validate
* @param dag - The workflow DAG
* @param executedBlocks - Set of blocks that were executed in the source run
* @returns Validation result with error message if invalid
*/
export function validateRunFromBlock(
blockId: string,
dag: DAG,
executedBlocks: Set<string>
): RunFromBlockValidation {
const node = dag.nodes.get(blockId)
const isLoopContainer = dag.loopConfigs.has(blockId)
const isParallelContainer = dag.parallelConfigs.has(blockId)
const isContainer = isLoopContainer || isParallelContainer
if (!node && !isContainer) {
return { valid: false, error: `Block not found in workflow: ${blockId}` }
}
if (isContainer) {
const sentinelStartId = resolveContainerToSentinelStart(blockId, dag)
if (!sentinelStartId || !dag.nodes.has(sentinelStartId)) {
return {
valid: false,
error: `Container sentinel not found for: ${blockId}`,
}
}
}
if (node) {
if (node.metadata.isLoopNode) {
return {
valid: false,
error: `Cannot run from block inside loop: ${node.metadata.loopId}`,
}
}
if (node.metadata.isParallelBranch) {
return {
valid: false,
error: `Cannot run from block inside parallel: ${node.metadata.parallelId}`,
}
}
if (node.metadata.isSentinel) {
return { valid: false, error: 'Cannot run from sentinel node' }
}
// Check immediate upstream dependencies were executed
for (const sourceId of node.incomingEdges) {
const sourceNode = dag.nodes.get(sourceId)
// Skip sentinel nodes - they're internal and not in executedBlocks
if (sourceNode?.metadata.isSentinel) continue
// Skip trigger nodes - they're entry points and don't need prior execution
// A trigger node has no incoming edges
if (sourceNode && sourceNode.incomingEdges.size === 0) continue
if (!executedBlocks.has(sourceId)) {
return {
valid: false,
error: `Upstream dependency not executed: ${sourceId}`,
}
}
}
}
return { valid: true }
}

View File

@@ -1,10 +1,85 @@
import { useCallback, useRef } from 'react'
import { createLogger } from '@sim/logger'
import type { ExecutionEvent } from '@/lib/workflows/executor/execution-events'
import type { SerializableExecutionState } from '@/executor/execution/types'
import type { SubflowType } from '@/stores/workflows/workflow/types'
const logger = createLogger('useExecutionStream')
/**
* Processes SSE events from a response body and invokes appropriate callbacks.
*/
async function processSSEStream(
reader: ReadableStreamDefaultReader<Uint8Array>,
callbacks: ExecutionStreamCallbacks,
logPrefix: string
): Promise<void> {
const decoder = new TextDecoder()
let buffer = ''
try {
while (true) {
const { done, value } = await reader.read()
if (done) break
buffer += decoder.decode(value, { stream: true })
const lines = buffer.split('\n\n')
buffer = lines.pop() || ''
for (const line of lines) {
if (!line.trim() || !line.startsWith('data: ')) continue
const data = line.substring(6).trim()
if (data === '[DONE]') {
logger.info(`${logPrefix} stream completed`)
continue
}
try {
const event = JSON.parse(data) as ExecutionEvent
switch (event.type) {
case 'execution:started':
callbacks.onExecutionStarted?.(event.data)
break
case 'execution:completed':
callbacks.onExecutionCompleted?.(event.data)
break
case 'execution:error':
callbacks.onExecutionError?.(event.data)
break
case 'execution:cancelled':
callbacks.onExecutionCancelled?.(event.data)
break
case 'block:started':
callbacks.onBlockStarted?.(event.data)
break
case 'block:completed':
callbacks.onBlockCompleted?.(event.data)
break
case 'block:error':
callbacks.onBlockError?.(event.data)
break
case 'stream:chunk':
callbacks.onStreamChunk?.(event.data)
break
case 'stream:done':
callbacks.onStreamDone?.(event.data)
break
default:
logger.warn('Unknown event type:', (event as any).type)
}
} catch (error) {
logger.error('Failed to parse SSE event:', error, { data })
}
}
}
} finally {
reader.releaseLock()
}
}
export interface ExecutionStreamCallbacks {
onExecutionStarted?: (data: { startTime: string }) => void
onExecutionCompleted?: (data: {
@@ -68,6 +143,15 @@ export interface ExecuteStreamOptions {
loops?: Record<string, any>
parallels?: Record<string, any>
}
stopAfterBlockId?: string
callbacks?: ExecutionStreamCallbacks
}
export interface ExecuteFromBlockOptions {
workflowId: string
startBlockId: string
sourceSnapshot: SerializableExecutionState
input?: any
callbacks?: ExecutionStreamCallbacks
}
@@ -119,91 +203,7 @@ export function useExecutionStream() {
}
const reader = response.body.getReader()
const decoder = new TextDecoder()
let buffer = ''
try {
while (true) {
const { done, value } = await reader.read()
if (done) {
break
}
buffer += decoder.decode(value, { stream: true })
const lines = buffer.split('\n\n')
buffer = lines.pop() || ''
for (const line of lines) {
if (!line.trim() || !line.startsWith('data: ')) {
continue
}
const data = line.substring(6).trim()
if (data === '[DONE]') {
logger.info('Stream completed')
continue
}
try {
const event = JSON.parse(data) as ExecutionEvent
logger.info('📡 SSE Event received:', {
type: event.type,
executionId: event.executionId,
data: event.data,
})
switch (event.type) {
case 'execution:started':
logger.info('🚀 Execution started')
callbacks.onExecutionStarted?.(event.data)
break
case 'execution:completed':
logger.info('✅ Execution completed')
callbacks.onExecutionCompleted?.(event.data)
break
case 'execution:error':
logger.error('❌ Execution error')
callbacks.onExecutionError?.(event.data)
break
case 'execution:cancelled':
logger.warn('🛑 Execution cancelled')
callbacks.onExecutionCancelled?.(event.data)
break
case 'block:started':
logger.info('🔷 Block started:', event.data.blockId)
callbacks.onBlockStarted?.(event.data)
break
case 'block:completed':
logger.info('✓ Block completed:', event.data.blockId)
callbacks.onBlockCompleted?.(event.data)
break
case 'block:error':
logger.error('✗ Block error:', event.data.blockId)
callbacks.onBlockError?.(event.data)
break
case 'stream:chunk':
callbacks.onStreamChunk?.(event.data)
break
case 'stream:done':
logger.info('Stream done:', event.data.blockId)
callbacks.onStreamDone?.(event.data)
break
default:
logger.warn('Unknown event type:', (event as any).type)
}
} catch (error) {
logger.error('Failed to parse SSE event:', error, { data })
}
}
}
} finally {
reader.releaseLock()
}
await processSSEStream(reader, callbacks, 'Execution')
} catch (error: any) {
if (error.name === 'AbortError') {
logger.info('Execution stream cancelled')
@@ -222,6 +222,70 @@ export function useExecutionStream() {
}
}, [])
const executeFromBlock = useCallback(async (options: ExecuteFromBlockOptions) => {
const { workflowId, startBlockId, sourceSnapshot, input, callbacks = {} } = options
if (abortControllerRef.current) {
abortControllerRef.current.abort()
}
const abortController = new AbortController()
abortControllerRef.current = abortController
currentExecutionRef.current = null
try {
const response = await fetch(`/api/workflows/${workflowId}/execute-from-block`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ startBlockId, sourceSnapshot, input }),
signal: abortController.signal,
})
if (!response.ok) {
let errorResponse: any
try {
errorResponse = await response.json()
} catch {
throw new Error(`Server error (${response.status}): ${response.statusText}`)
}
const error = new Error(errorResponse.error || 'Failed to start execution')
if (errorResponse && typeof errorResponse === 'object') {
Object.assign(error, { executionResult: errorResponse })
}
throw error
}
if (!response.body) {
throw new Error('No response body')
}
const executionId = response.headers.get('X-Execution-Id')
if (executionId) {
currentExecutionRef.current = { workflowId, executionId }
}
const reader = response.body.getReader()
await processSSEStream(reader, callbacks, 'Run-from-block')
} catch (error: any) {
if (error.name === 'AbortError') {
logger.info('Run-from-block execution cancelled')
callbacks.onExecutionCancelled?.({ duration: 0 })
} else {
logger.error('Run-from-block execution error:', error)
callbacks.onExecutionError?.({
error: error.message || 'Unknown error',
duration: 0,
})
}
throw error
} finally {
abortControllerRef.current = null
currentExecutionRef.current = null
}
}, [])
const cancel = useCallback(() => {
const execution = currentExecutionRef.current
if (execution) {
@@ -239,6 +303,7 @@ export function useExecutionStream() {
return {
execute,
executeFromBlock,
cancel,
}
}

View File

@@ -4,6 +4,7 @@ import { createLogger } from '@sim/logger'
import { and, eq, isNull } from 'drizzle-orm'
import { loadWorkflowFromNormalizedTables } from '@/lib/workflows/persistence/utils'
import { sanitizeForCopilot } from '@/lib/workflows/sanitization/json-sanitizer'
import { isHiddenFromDisplay } from '@/blocks/types'
import { escapeRegExp } from '@/executor/constants'
import { getUserPermissionConfig } from '@/executor/utils/permission-check'
import type { ChatContext } from '@/stores/panel/copilot/types'
@@ -397,7 +398,11 @@ async function processBlockMetadata(
category: blockConfig.category,
bgColor: blockConfig.bgColor,
inputs: blockConfig.inputs || {},
outputs: blockConfig.outputs || {},
outputs: blockConfig.outputs
? Object.fromEntries(
Object.entries(blockConfig.outputs).filter(([_, def]) => !isHiddenFromDisplay(def))
)
: {},
tools: blockConfig.tools?.access || [],
hideFromToolbar: blockConfig.hideFromToolbar,
}

View File

@@ -6,7 +6,7 @@ import {
type GetBlockConfigResultType,
} from '@/lib/copilot/tools/shared/schemas'
import { registry as blockRegistry, getLatestBlock } from '@/blocks/registry'
import type { SubBlockConfig } from '@/blocks/types'
import { isHiddenFromDisplay, type SubBlockConfig } from '@/blocks/types'
import { getUserPermissionConfig } from '@/executor/utils/permission-check'
import { PROVIDER_DEFINITIONS } from '@/providers/models'
import { tools as toolsRegistry } from '@/tools/registry'
@@ -310,6 +310,7 @@ function extractTriggerOutputs(blockConfig: any): Record<string, OutputFieldSche
const trigger = getTrigger(triggerId)
if (trigger.outputs) {
for (const [key, def] of Object.entries(trigger.outputs)) {
if (isHiddenFromDisplay(def)) continue
outputs[key] = extractOutputField(def)
}
}
@@ -342,6 +343,7 @@ function extractOutputs(
const tool = toolsRegistry[toolId]
if (tool?.outputs) {
for (const [key, def] of Object.entries(tool.outputs)) {
if (isHiddenFromDisplay(def)) continue
outputs[key] = extractOutputField(def)
}
return outputs
@@ -355,6 +357,7 @@ function extractOutputs(
// Use block-level outputs
if (blockConfig.outputs) {
for (const [key, def] of Object.entries(blockConfig.outputs)) {
if (isHiddenFromDisplay(def)) continue
outputs[key] = extractOutputField(def)
}
}

View File

@@ -7,8 +7,7 @@ import {
GetBlocksMetadataResult,
} from '@/lib/copilot/tools/shared/schemas'
import { registry as blockRegistry } from '@/blocks/registry'
import type { BlockConfig } from '@/blocks/types'
import { AuthMode } from '@/blocks/types'
import { AuthMode, type BlockConfig, isHiddenFromDisplay } from '@/blocks/types'
import { getUserPermissionConfig } from '@/executor/utils/permission-check'
import { PROVIDER_DEFINITIONS } from '@/providers/models'
import { tools as toolsRegistry } from '@/tools/registry'
@@ -249,6 +248,12 @@ export const getBlocksMetadataServerTool: BaseServerTool<
}
}
const filteredOutputs = blockConfig.outputs
? Object.fromEntries(
Object.entries(blockConfig.outputs).filter(([_, def]) => !isHiddenFromDisplay(def))
)
: undefined
metadata = {
id: blockId,
name: blockConfig.name || blockId,
@@ -262,7 +267,7 @@ export const getBlocksMetadataServerTool: BaseServerTool<
triggers,
operationInputSchema: operationParameters,
operations,
outputs: blockConfig.outputs,
outputs: filteredOutputs,
}
}

View File

@@ -16,7 +16,12 @@ import {
USER_FILE_PROPERTY_TYPES,
} from '@/lib/workflows/types'
import { getBlock } from '@/blocks'
import type { BlockConfig, OutputCondition, OutputFieldDefinition } from '@/blocks/types'
import {
type BlockConfig,
isHiddenFromDisplay,
type OutputCondition,
type OutputFieldDefinition,
} from '@/blocks/types'
import { getTool } from '@/tools/utils'
import { getTrigger, isTriggerValid } from '@/triggers'
@@ -86,8 +91,8 @@ function evaluateOutputCondition(
}
/**
* Filters outputs based on their conditions.
* Returns a new OutputDefinition with only the outputs whose conditions are met.
* Filters outputs based on their conditions and hiddenFromDisplay flag.
* Returns a new OutputDefinition with only the outputs that should be shown.
*/
function filterOutputsByCondition(
outputs: OutputDefinition,
@@ -96,6 +101,8 @@ function filterOutputsByCondition(
const filtered: OutputDefinition = {}
for (const [key, value] of Object.entries(outputs)) {
if (isHiddenFromDisplay(value)) continue
if (!value || typeof value !== 'object' || !('condition' in value)) {
filtered[key] = value
continue
@@ -105,7 +112,7 @@ function filterOutputsByCondition(
const passes = !condition || evaluateOutputCondition(condition, subBlocks)
if (passes) {
const { condition: _, ...rest } = value
const { condition: _, hiddenFromDisplay: __, ...rest } = value
filtered[key] = rest
}
}
@@ -259,50 +266,26 @@ export function getBlockOutputs(
}
if (blockType === 'human_in_the_loop') {
const hitlOutputs: OutputDefinition = {
url: { type: 'string', description: 'Resume UI URL' },
resumeEndpoint: {
type: 'string',
description: 'Resume API endpoint URL for direct curl requests',
},
}
// Start with block config outputs (respects hiddenFromDisplay via filterOutputsByCondition)
const baseOutputs = filterOutputsByCondition(
{ ...(blockConfig.outputs || {}) } as OutputDefinition,
subBlocks
)
// Add inputFormat fields (resume form fields)
const normalizedInputFormat = normalizeInputFormatValue(subBlocks?.inputFormat?.value)
for (const field of normalizedInputFormat) {
const fieldName = field?.name?.trim()
if (!fieldName) continue
hitlOutputs[fieldName] = {
baseOutputs[fieldName] = {
type: (field?.type || 'any') as any,
description: `Field from resume form`,
description: field?.description || `Field from resume form`,
}
}
return hitlOutputs
}
if (blockType === 'approval') {
// Start with only url (apiUrl commented out - not accessible as output)
const pauseResumeOutputs: OutputDefinition = {
url: { type: 'string', description: 'Resume UI URL' },
// apiUrl: { type: 'string', description: 'Resume API URL' }, // Commented out - not accessible as output
}
const normalizedInputFormat = normalizeInputFormatValue(subBlocks?.inputFormat?.value)
// Add each input format field as a top-level output
for (const field of normalizedInputFormat) {
const fieldName = field?.name?.trim()
if (!fieldName) continue
pauseResumeOutputs[fieldName] = {
type: (field?.type || 'any') as any,
description: `Field from input format`,
}
}
return pauseResumeOutputs
return baseOutputs
}
if (startPath === StartBlockPath.LEGACY_STARTER) {

View File

@@ -23,9 +23,11 @@ import type {
ContextExtensions,
ExecutionCallbacks,
IterationContext,
SerializableExecutionState,
} from '@/executor/execution/types'
import type { ExecutionResult, NormalizedBlockOutput } from '@/executor/types'
import { hasExecutionResult } from '@/executor/utils/errors'
import { buildParallelSentinelEndId, buildSentinelEndId } from '@/executor/utils/subflow-utils'
import { Serializer } from '@/serializer'
const logger = createLogger('ExecutionCore')
@@ -40,6 +42,12 @@ export interface ExecuteWorkflowCoreOptions {
abortSignal?: AbortSignal
includeFileBase64?: boolean
base64MaxBytes?: number
stopAfterBlockId?: string
/** Run-from-block mode: execute starting from a specific block using cached upstream outputs */
runFromBlock?: {
startBlockId: string
sourceSnapshot: SerializableExecutionState
}
}
function parseVariableValueByType(value: unknown, type: string): unknown {
@@ -114,6 +122,8 @@ export async function executeWorkflowCore(
abortSignal,
includeFileBase64,
base64MaxBytes,
stopAfterBlockId,
runFromBlock,
} = options
const { metadata, workflow, input, workflowVariables, selectedOutputs } = snapshot
const { requestId, workflowId, userId, triggerType, executionId, triggerBlockId, useDraftState } =
@@ -246,6 +256,16 @@ export async function executeWorkflowCore(
processedInput = input || {}
// Resolve stopAfterBlockId for loop/parallel containers to their sentinel-end IDs
let resolvedStopAfterBlockId = stopAfterBlockId
if (stopAfterBlockId) {
if (serializedWorkflow.loops?.[stopAfterBlockId]) {
resolvedStopAfterBlockId = buildSentinelEndId(stopAfterBlockId)
} else if (serializedWorkflow.parallels?.[stopAfterBlockId]) {
resolvedStopAfterBlockId = buildParallelSentinelEndId(stopAfterBlockId)
}
}
// Create and execute workflow with callbacks
if (resumeFromSnapshot) {
logger.info(`[${requestId}] Resume execution detected`, {
@@ -296,6 +316,7 @@ export async function executeWorkflowCore(
abortSignal,
includeFileBase64,
base64MaxBytes,
stopAfterBlockId: resolvedStopAfterBlockId,
}
const executorInstance = new Executor({
@@ -318,10 +339,13 @@ export async function executeWorkflowCore(
}
}
const result = (await executorInstance.execute(
workflowId,
resolvedTriggerBlockId
)) as ExecutionResult
const result = runFromBlock
? ((await executorInstance.executeFromBlock(
workflowId,
runFromBlock.startBlockId,
runFromBlock.sourceSnapshot
)) as ExecutionResult)
: ((await executorInstance.execute(workflowId, resolvedTriggerBlockId)) as ExecutionResult)
// Build trace spans for logging from the full execution result
const { traceSpans, totalDuration } = buildTraceSpans(result)

View File

@@ -180,3 +180,140 @@ export function formatSSEEvent(event: ExecutionEvent): string {
export function encodeSSEEvent(event: ExecutionEvent): Uint8Array {
return new TextEncoder().encode(formatSSEEvent(event))
}
/**
* Options for creating SSE execution callbacks
*/
export interface SSECallbackOptions {
executionId: string
workflowId: string
controller: ReadableStreamDefaultController<Uint8Array>
isStreamClosed: () => boolean
setStreamClosed: () => void
}
/**
* Creates SSE callbacks for workflow execution streaming
*/
export function createSSECallbacks(options: SSECallbackOptions) {
const { executionId, workflowId, controller, isStreamClosed, setStreamClosed } = options
const sendEvent = (event: ExecutionEvent) => {
if (isStreamClosed()) return
try {
controller.enqueue(encodeSSEEvent(event))
} catch {
setStreamClosed()
}
}
const onBlockStart = async (
blockId: string,
blockName: string,
blockType: string,
iterationContext?: { iterationCurrent: number; iterationTotal: number; iterationType: string }
) => {
sendEvent({
type: 'block:started',
timestamp: new Date().toISOString(),
executionId,
workflowId,
data: {
blockId,
blockName,
blockType,
...(iterationContext && {
iterationCurrent: iterationContext.iterationCurrent,
iterationTotal: iterationContext.iterationTotal,
iterationType: iterationContext.iterationType as any,
}),
},
})
}
const onBlockComplete = async (
blockId: string,
blockName: string,
blockType: string,
callbackData: { input?: unknown; output: any; executionTime: number },
iterationContext?: { iterationCurrent: number; iterationTotal: number; iterationType: string }
) => {
const hasError = callbackData.output?.error
const iterationData = iterationContext
? {
iterationCurrent: iterationContext.iterationCurrent,
iterationTotal: iterationContext.iterationTotal,
iterationType: iterationContext.iterationType as any,
}
: {}
if (hasError) {
sendEvent({
type: 'block:error',
timestamp: new Date().toISOString(),
executionId,
workflowId,
data: {
blockId,
blockName,
blockType,
input: callbackData.input,
error: callbackData.output.error,
durationMs: callbackData.executionTime || 0,
...iterationData,
},
})
} else {
sendEvent({
type: 'block:completed',
timestamp: new Date().toISOString(),
executionId,
workflowId,
data: {
blockId,
blockName,
blockType,
input: callbackData.input,
output: callbackData.output,
durationMs: callbackData.executionTime || 0,
...iterationData,
},
})
}
}
const onStream = async (streamingExecution: unknown) => {
const streamingExec = streamingExecution as { stream: ReadableStream; execution: any }
const blockId = streamingExec.execution?.blockId
const reader = streamingExec.stream.getReader()
const decoder = new TextDecoder()
try {
while (true) {
const { done, value } = await reader.read()
if (done) break
const chunk = decoder.decode(value, { stream: true })
sendEvent({
type: 'stream:chunk',
timestamp: new Date().toISOString(),
executionId,
workflowId,
data: { blockId, chunk },
})
}
sendEvent({
type: 'stream:done',
timestamp: new Date().toISOString(),
executionId,
workflowId,
data: { blockId },
})
} finally {
try {
reader.releaseLock()
} catch {}
}
}
return { sendEvent, onBlockStart, onBlockComplete, onStream }
}

View File

@@ -9,6 +9,7 @@ import { LoggingSession } from '@/lib/logs/execution/logging-session'
import { executeWorkflowCore } from '@/lib/workflows/executor/execution-core'
import { ExecutionSnapshot } from '@/executor/execution/snapshot'
import type { ExecutionResult, PausePoint, SerializedSnapshot } from '@/executor/types'
import { filterOutputForLog } from '@/executor/utils/output-filter'
import type { SerializedConnection } from '@/serializer/types'
const logger = createLogger('HumanInTheLoopManager')
@@ -576,13 +577,11 @@ export class PauseResumeManager {
log.blockId === contextId
)
if (blockLogIndex !== -1) {
// Filter output for logging (exclude internal fields and response)
const filteredOutput: Record<string, unknown> = {}
for (const [key, value] of Object.entries(mergedOutput)) {
if (key.startsWith('_')) continue
if (key === 'response') continue
filteredOutput[key] = value
}
// Filter output for logging using shared utility
// 'resume' is redundant with url/resumeEndpoint so we filter it out
const filteredOutput = filterOutputForLog('human_in_the_loop', mergedOutput, {
additionalHiddenKeys: ['resume'],
})
stateCopy.blockLogs[blockLogIndex] = {
...stateCopy.blockLogs[blockLogIndex],
blockId: stateBlockKey,

View File

@@ -35,4 +35,23 @@ export const useExecutionStore = create<ExecutionState & ExecutionActions>()((se
},
clearRunPath: () => set({ lastRunPath: new Map(), lastRunEdges: new Map() }),
reset: () => set(initialState),
setLastExecutionSnapshot: (workflowId, snapshot) => {
const { lastExecutionSnapshots } = get()
const newSnapshots = new Map(lastExecutionSnapshots)
newSnapshots.set(workflowId, snapshot)
set({ lastExecutionSnapshots: newSnapshots })
},
getLastExecutionSnapshot: (workflowId) => {
const { lastExecutionSnapshots } = get()
return lastExecutionSnapshots.get(workflowId)
},
clearLastExecutionSnapshot: (workflowId) => {
const { lastExecutionSnapshots } = get()
const newSnapshots = new Map(lastExecutionSnapshots)
newSnapshots.delete(workflowId)
set({ lastExecutionSnapshots: newSnapshots })
},
}))

View File

@@ -1,4 +1,5 @@
import type { Executor } from '@/executor'
import type { SerializableExecutionState } from '@/executor/execution/types'
import type { ExecutionContext } from '@/executor/types'
/**
@@ -18,16 +19,9 @@ export interface ExecutionState {
pendingBlocks: string[]
executor: Executor | null
debugContext: ExecutionContext | null
/**
* Tracks blocks from the last execution run and their success/error status.
* Cleared when a new run starts. Used to show run path indicators (rings on blocks).
*/
lastRunPath: Map<string, BlockRunStatus>
/**
* Tracks edges from the last execution run and their success/error status.
* Cleared when a new run starts. Used to show run path indicators on edges.
*/
lastRunEdges: Map<string, EdgeRunStatus>
lastExecutionSnapshots: Map<string, SerializableExecutionState>
}
export interface ExecutionActions {
@@ -41,6 +35,9 @@ export interface ExecutionActions {
setEdgeRunStatus: (edgeId: string, status: EdgeRunStatus) => void
clearRunPath: () => void
reset: () => void
setLastExecutionSnapshot: (workflowId: string, snapshot: SerializableExecutionState) => void
getLastExecutionSnapshot: (workflowId: string) => SerializableExecutionState | undefined
clearLastExecutionSnapshot: (workflowId: string) => void
}
export const initialState: ExecutionState = {
@@ -52,4 +49,5 @@ export const initialState: ExecutionState = {
debugContext: null,
lastRunPath: new Map(),
lastRunEdges: new Map(),
lastExecutionSnapshots: new Map(),
}

View File

@@ -0,0 +1,146 @@
import { createLogger } from '@sim/logger'
import type {
ClerkApiError,
ClerkCreateOrganizationParams,
ClerkCreateOrganizationResponse,
ClerkOrganization,
} from '@/tools/clerk/types'
import type { ToolConfig } from '@/tools/types'
const logger = createLogger('ClerkCreateOrganization')
export const clerkCreateOrganizationTool: ToolConfig<
ClerkCreateOrganizationParams,
ClerkCreateOrganizationResponse
> = {
id: 'clerk_create_organization',
name: 'Create Organization in Clerk',
description: 'Create a new organization in your Clerk application',
version: '1.0.0',
params: {
secretKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'The Clerk Secret Key for API authentication',
},
name: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Name of the organization',
},
createdBy: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'User ID of the creator (will become admin)',
},
slug: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Slug identifier for the organization',
},
maxAllowedMemberships: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description: 'Maximum member capacity (0 for unlimited)',
},
publicMetadata: {
type: 'json',
required: false,
visibility: 'user-or-llm',
description: 'Public metadata (JSON object)',
},
privateMetadata: {
type: 'json',
required: false,
visibility: 'user-or-llm',
description: 'Private metadata (JSON object)',
},
},
request: {
url: () => 'https://api.clerk.com/v1/organizations',
method: 'POST',
headers: (params) => {
if (!params.secretKey) {
throw new Error('Clerk Secret Key is required')
}
return {
Authorization: `Bearer ${params.secretKey}`,
'Content-Type': 'application/json',
}
},
body: (params) => {
const body: Record<string, unknown> = {
name: params.name,
created_by: params.createdBy,
}
if (params.slug) body.slug = params.slug
if (params.maxAllowedMemberships !== undefined)
body.max_allowed_memberships = params.maxAllowedMemberships
if (params.publicMetadata) body.public_metadata = params.publicMetadata
if (params.privateMetadata) body.private_metadata = params.privateMetadata
return body
},
},
transformResponse: async (response: Response) => {
const data: ClerkOrganization | ClerkApiError = await response.json()
if (!response.ok) {
logger.error('Clerk API request failed', { data, status: response.status })
throw new Error(
(data as ClerkApiError).errors?.[0]?.message || 'Failed to create organization in Clerk'
)
}
const org = data as ClerkOrganization
return {
success: true,
output: {
id: org.id,
name: org.name,
slug: org.slug ?? null,
imageUrl: org.image_url ?? null,
hasImage: org.has_image ?? false,
membersCount: org.members_count ?? null,
pendingInvitationsCount: org.pending_invitations_count ?? null,
maxAllowedMemberships: org.max_allowed_memberships ?? 0,
adminDeleteEnabled: org.admin_delete_enabled ?? false,
createdBy: org.created_by ?? null,
createdAt: org.created_at,
updatedAt: org.updated_at,
publicMetadata: org.public_metadata ?? {},
success: true,
},
}
},
outputs: {
id: { type: 'string', description: 'Created organization ID' },
name: { type: 'string', description: 'Organization name' },
slug: { type: 'string', description: 'Organization slug', optional: true },
imageUrl: { type: 'string', description: 'Organization image URL', optional: true },
hasImage: { type: 'boolean', description: 'Whether organization has an image' },
membersCount: { type: 'number', description: 'Number of members', optional: true },
pendingInvitationsCount: {
type: 'number',
description: 'Number of pending invitations',
optional: true,
},
maxAllowedMemberships: { type: 'number', description: 'Max allowed memberships' },
adminDeleteEnabled: { type: 'boolean', description: 'Whether admin delete is enabled' },
createdBy: { type: 'string', description: 'Creator user ID', optional: true },
createdAt: { type: 'number', description: 'Creation timestamp' },
updatedAt: { type: 'number', description: 'Last update timestamp' },
publicMetadata: { type: 'json', description: 'Public metadata' },
success: { type: 'boolean', description: 'Operation success status' },
},
}

View File

@@ -0,0 +1,227 @@
import { createLogger } from '@sim/logger'
import type {
ClerkApiError,
ClerkCreateUserParams,
ClerkCreateUserResponse,
ClerkEmailAddress,
ClerkPhoneNumber,
ClerkUser,
} from '@/tools/clerk/types'
import type { ToolConfig } from '@/tools/types'
const logger = createLogger('ClerkCreateUser')
export const clerkCreateUserTool: ToolConfig<ClerkCreateUserParams, ClerkCreateUserResponse> = {
id: 'clerk_create_user',
name: 'Create User in Clerk',
description: 'Create a new user in your Clerk application',
version: '1.0.0',
params: {
secretKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'The Clerk Secret Key for API authentication',
},
emailAddress: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Email addresses for the user (comma-separated for multiple)',
},
phoneNumber: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Phone numbers for the user (comma-separated for multiple)',
},
username: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Username for the user (must be unique)',
},
password: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Password for the user (minimum 8 characters)',
},
firstName: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'First name of the user',
},
lastName: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Last name of the user',
},
externalId: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'External system identifier (must be unique)',
},
publicMetadata: {
type: 'json',
required: false,
visibility: 'user-or-llm',
description: 'Public metadata (JSON object, readable from frontend)',
},
privateMetadata: {
type: 'json',
required: false,
visibility: 'user-or-llm',
description: 'Private metadata (JSON object, backend only)',
},
unsafeMetadata: {
type: 'json',
required: false,
visibility: 'user-or-llm',
description: 'Unsafe metadata (JSON object, modifiable from frontend)',
},
skipPasswordChecks: {
type: 'boolean',
required: false,
visibility: 'user-or-llm',
description: 'Skip password validation checks',
},
skipPasswordRequirement: {
type: 'boolean',
required: false,
visibility: 'user-or-llm',
description: 'Make password optional',
},
},
request: {
url: () => 'https://api.clerk.com/v1/users',
method: 'POST',
headers: (params) => {
if (!params.secretKey) {
throw new Error('Clerk Secret Key is required')
}
return {
Authorization: `Bearer ${params.secretKey}`,
'Content-Type': 'application/json',
}
},
body: (params) => {
const body: Record<string, unknown> = {}
if (params.emailAddress) {
const emailStr = params.emailAddress as string
body.email_address = emailStr.split(',').map((e) => e.trim())
}
if (params.phoneNumber) {
const phoneStr = params.phoneNumber as string
body.phone_number = phoneStr.split(',').map((p) => p.trim())
}
if (params.username) body.username = params.username.trim()
if (params.password) body.password = params.password
if (params.firstName) body.first_name = params.firstName.trim()
if (params.lastName) body.last_name = params.lastName.trim()
if (params.externalId) body.external_id = params.externalId.trim()
if (params.publicMetadata) body.public_metadata = params.publicMetadata
if (params.privateMetadata) body.private_metadata = params.privateMetadata
if (params.unsafeMetadata) body.unsafe_metadata = params.unsafeMetadata
if (params.skipPasswordChecks !== undefined)
body.skip_password_checks = params.skipPasswordChecks
if (params.skipPasswordRequirement !== undefined)
body.skip_password_requirement = params.skipPasswordRequirement
return body
},
},
transformResponse: async (response: Response) => {
const data: ClerkUser | ClerkApiError = await response.json()
if (!response.ok) {
logger.error('Clerk API request failed', { data, status: response.status })
throw new Error(
(data as ClerkApiError).errors?.[0]?.message || 'Failed to create user in Clerk'
)
}
const user = data as ClerkUser
return {
success: true,
output: {
id: user.id,
username: user.username ?? null,
firstName: user.first_name ?? null,
lastName: user.last_name ?? null,
imageUrl: user.image_url ?? null,
primaryEmailAddressId: user.primary_email_address_id ?? null,
primaryPhoneNumberId: user.primary_phone_number_id ?? null,
emailAddresses: (user.email_addresses ?? []).map((email: ClerkEmailAddress) => ({
id: email.id,
emailAddress: email.email_address,
verified: email.verification?.status === 'verified',
})),
phoneNumbers: (user.phone_numbers ?? []).map((phone: ClerkPhoneNumber) => ({
id: phone.id,
phoneNumber: phone.phone_number,
verified: phone.verification?.status === 'verified',
})),
externalId: user.external_id ?? null,
createdAt: user.created_at,
updatedAt: user.updated_at,
publicMetadata: user.public_metadata ?? {},
success: true,
},
}
},
outputs: {
id: { type: 'string', description: 'Created user ID' },
username: { type: 'string', description: 'Username', optional: true },
firstName: { type: 'string', description: 'First name', optional: true },
lastName: { type: 'string', description: 'Last name', optional: true },
imageUrl: { type: 'string', description: 'Profile image URL', optional: true },
primaryEmailAddressId: {
type: 'string',
description: 'Primary email address ID',
optional: true,
},
primaryPhoneNumberId: {
type: 'string',
description: 'Primary phone number ID',
optional: true,
},
emailAddresses: {
type: 'array',
description: 'User email addresses',
items: {
type: 'object',
properties: {
id: { type: 'string', description: 'Email address ID' },
emailAddress: { type: 'string', description: 'Email address' },
verified: { type: 'boolean', description: 'Whether email is verified' },
},
},
},
phoneNumbers: {
type: 'array',
description: 'User phone numbers',
items: {
type: 'object',
properties: {
id: { type: 'string', description: 'Phone number ID' },
phoneNumber: { type: 'string', description: 'Phone number' },
verified: { type: 'boolean', description: 'Whether phone is verified' },
},
},
},
externalId: { type: 'string', description: 'External system ID', optional: true },
createdAt: { type: 'number', description: 'Creation timestamp' },
updatedAt: { type: 'number', description: 'Last update timestamp' },
publicMetadata: { type: 'json', description: 'Public metadata' },
success: { type: 'boolean', description: 'Operation success status' },
},
}

View File

@@ -0,0 +1,75 @@
import { createLogger } from '@sim/logger'
import type {
ClerkApiError,
ClerkDeleteResponse,
ClerkDeleteUserParams,
ClerkDeleteUserResponse,
} from '@/tools/clerk/types'
import type { ToolConfig } from '@/tools/types'
const logger = createLogger('ClerkDeleteUser')
export const clerkDeleteUserTool: ToolConfig<ClerkDeleteUserParams, ClerkDeleteUserResponse> = {
id: 'clerk_delete_user',
name: 'Delete User from Clerk',
description: 'Delete a user from your Clerk application',
version: '1.0.0',
params: {
secretKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'The Clerk Secret Key for API authentication',
},
userId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'The ID of the user to delete',
},
},
request: {
url: (params) => `https://api.clerk.com/v1/users/${params.userId?.trim()}`,
method: 'DELETE',
headers: (params) => {
if (!params.secretKey) {
throw new Error('Clerk Secret Key is required')
}
return {
Authorization: `Bearer ${params.secretKey}`,
'Content-Type': 'application/json',
}
},
},
transformResponse: async (response: Response) => {
const data: ClerkDeleteResponse | ClerkApiError = await response.json()
if (!response.ok) {
logger.error('Clerk API request failed', { data, status: response.status })
throw new Error(
(data as ClerkApiError).errors?.[0]?.message || 'Failed to delete user from Clerk'
)
}
const deleteData = data as ClerkDeleteResponse
return {
success: true,
output: {
id: deleteData.id,
object: deleteData.object ?? 'user',
deleted: deleteData.deleted ?? true,
success: true,
},
}
},
outputs: {
id: { type: 'string', description: 'Deleted user ID' },
object: { type: 'string', description: 'Object type (user)' },
deleted: { type: 'boolean', description: 'Whether the user was deleted' },
success: { type: 'boolean', description: 'Operation success status' },
},
}

View File

@@ -0,0 +1,102 @@
import { createLogger } from '@sim/logger'
import type {
ClerkApiError,
ClerkGetOrganizationParams,
ClerkGetOrganizationResponse,
ClerkOrganization,
} from '@/tools/clerk/types'
import type { ToolConfig } from '@/tools/types'
const logger = createLogger('ClerkGetOrganization')
export const clerkGetOrganizationTool: ToolConfig<
ClerkGetOrganizationParams,
ClerkGetOrganizationResponse
> = {
id: 'clerk_get_organization',
name: 'Get Organization from Clerk',
description: 'Retrieve a single organization by ID or slug from Clerk',
version: '1.0.0',
params: {
secretKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'The Clerk Secret Key for API authentication',
},
organizationId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'The ID or slug of the organization to retrieve',
},
},
request: {
url: (params) => `https://api.clerk.com/v1/organizations/${params.organizationId}`,
method: 'GET',
headers: (params) => {
if (!params.secretKey) {
throw new Error('Clerk Secret Key is required')
}
return {
Authorization: `Bearer ${params.secretKey}`,
'Content-Type': 'application/json',
}
},
},
transformResponse: async (response: Response) => {
const data: ClerkOrganization | ClerkApiError = await response.json()
if (!response.ok) {
logger.error('Clerk API request failed', { data, status: response.status })
throw new Error(
(data as ClerkApiError).errors?.[0]?.message || 'Failed to get organization from Clerk'
)
}
const org = data as ClerkOrganization
return {
success: true,
output: {
id: org.id,
name: org.name,
slug: org.slug ?? null,
imageUrl: org.image_url ?? null,
hasImage: org.has_image ?? false,
membersCount: org.members_count ?? null,
pendingInvitationsCount: org.pending_invitations_count ?? null,
maxAllowedMemberships: org.max_allowed_memberships ?? 0,
adminDeleteEnabled: org.admin_delete_enabled ?? false,
createdBy: org.created_by ?? null,
createdAt: org.created_at,
updatedAt: org.updated_at,
publicMetadata: org.public_metadata ?? {},
success: true,
},
}
},
outputs: {
id: { type: 'string', description: 'Organization ID' },
name: { type: 'string', description: 'Organization name' },
slug: { type: 'string', description: 'Organization slug', optional: true },
imageUrl: { type: 'string', description: 'Organization image URL', optional: true },
hasImage: { type: 'boolean', description: 'Whether organization has an image' },
membersCount: { type: 'number', description: 'Number of members', optional: true },
pendingInvitationsCount: {
type: 'number',
description: 'Number of pending invitations',
optional: true,
},
maxAllowedMemberships: { type: 'number', description: 'Max allowed memberships' },
adminDeleteEnabled: { type: 'boolean', description: 'Whether admin delete is enabled' },
createdBy: { type: 'string', description: 'Creator user ID', optional: true },
createdAt: { type: 'number', description: 'Creation timestamp' },
updatedAt: { type: 'number', description: 'Last update timestamp' },
publicMetadata: { type: 'json', description: 'Public metadata' },
success: { type: 'boolean', description: 'Operation success status' },
},
}

View File

@@ -0,0 +1,93 @@
import { createLogger } from '@sim/logger'
import type {
ClerkApiError,
ClerkGetSessionParams,
ClerkGetSessionResponse,
ClerkSession,
} from '@/tools/clerk/types'
import type { ToolConfig } from '@/tools/types'
const logger = createLogger('ClerkGetSession')
export const clerkGetSessionTool: ToolConfig<ClerkGetSessionParams, ClerkGetSessionResponse> = {
id: 'clerk_get_session',
name: 'Get Session from Clerk',
description: 'Retrieve a single session by ID from Clerk',
version: '1.0.0',
params: {
secretKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'The Clerk Secret Key for API authentication',
},
sessionId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'The ID of the session to retrieve',
},
},
request: {
url: (params) => `https://api.clerk.com/v1/sessions/${params.sessionId}`,
method: 'GET',
headers: (params) => {
if (!params.secretKey) {
throw new Error('Clerk Secret Key is required')
}
return {
Authorization: `Bearer ${params.secretKey}`,
'Content-Type': 'application/json',
}
},
},
transformResponse: async (response: Response) => {
const data: ClerkSession | ClerkApiError = await response.json()
if (!response.ok) {
logger.error('Clerk API request failed', { data, status: response.status })
throw new Error(
(data as ClerkApiError).errors?.[0]?.message || 'Failed to get session from Clerk'
)
}
const session = data as ClerkSession
return {
success: true,
output: {
id: session.id,
userId: session.user_id,
clientId: session.client_id,
status: session.status,
lastActiveAt: session.last_active_at ?? null,
lastActiveOrganizationId: session.last_active_organization_id ?? null,
expireAt: session.expire_at ?? null,
abandonAt: session.abandon_at ?? null,
createdAt: session.created_at,
updatedAt: session.updated_at,
success: true,
},
}
},
outputs: {
id: { type: 'string', description: 'Session ID' },
userId: { type: 'string', description: 'User ID' },
clientId: { type: 'string', description: 'Client ID' },
status: { type: 'string', description: 'Session status' },
lastActiveAt: { type: 'number', description: 'Last activity timestamp', optional: true },
lastActiveOrganizationId: {
type: 'string',
description: 'Last active organization ID',
optional: true,
},
expireAt: { type: 'number', description: 'Expiration timestamp', optional: true },
abandonAt: { type: 'number', description: 'Abandon timestamp', optional: true },
createdAt: { type: 'number', description: 'Creation timestamp' },
updatedAt: { type: 'number', description: 'Last update timestamp' },
success: { type: 'boolean', description: 'Operation success status' },
},
}

View File

@@ -0,0 +1,167 @@
import { createLogger } from '@sim/logger'
import type {
ClerkApiError,
ClerkEmailAddress,
ClerkGetUserParams,
ClerkGetUserResponse,
ClerkPhoneNumber,
ClerkUser,
} from '@/tools/clerk/types'
import type { ToolConfig } from '@/tools/types'
const logger = createLogger('ClerkGetUser')
export const clerkGetUserTool: ToolConfig<ClerkGetUserParams, ClerkGetUserResponse> = {
id: 'clerk_get_user',
name: 'Get User from Clerk',
description: 'Retrieve a single user by their ID from Clerk',
version: '1.0.0',
params: {
secretKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'The Clerk Secret Key for API authentication',
},
userId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'The ID of the user to retrieve',
},
},
request: {
url: (params) => `https://api.clerk.com/v1/users/${params.userId?.trim()}`,
method: 'GET',
headers: (params) => {
if (!params.secretKey) {
throw new Error('Clerk Secret Key is required')
}
return {
Authorization: `Bearer ${params.secretKey}`,
'Content-Type': 'application/json',
}
},
},
transformResponse: async (response: Response) => {
const data: ClerkUser | ClerkApiError = await response.json()
if (!response.ok) {
logger.error('Clerk API request failed', { data, status: response.status })
throw new Error(
(data as ClerkApiError).errors?.[0]?.message || 'Failed to get user from Clerk'
)
}
const user = data as ClerkUser
return {
success: true,
output: {
id: user.id,
username: user.username ?? null,
firstName: user.first_name ?? null,
lastName: user.last_name ?? null,
imageUrl: user.image_url ?? null,
hasImage: user.has_image ?? false,
primaryEmailAddressId: user.primary_email_address_id ?? null,
primaryPhoneNumberId: user.primary_phone_number_id ?? null,
primaryWeb3WalletId: user.primary_web3_wallet_id ?? null,
emailAddresses: (user.email_addresses ?? []).map((email: ClerkEmailAddress) => ({
id: email.id,
emailAddress: email.email_address,
verified: email.verification?.status === 'verified',
})),
phoneNumbers: (user.phone_numbers ?? []).map((phone: ClerkPhoneNumber) => ({
id: phone.id,
phoneNumber: phone.phone_number,
verified: phone.verification?.status === 'verified',
})),
externalId: user.external_id ?? null,
passwordEnabled: user.password_enabled ?? false,
twoFactorEnabled: user.two_factor_enabled ?? false,
totpEnabled: user.totp_enabled ?? false,
backupCodeEnabled: user.backup_code_enabled ?? false,
banned: user.banned ?? false,
locked: user.locked ?? false,
deleteSelfEnabled: user.delete_self_enabled ?? false,
createOrganizationEnabled: user.create_organization_enabled ?? false,
lastSignInAt: user.last_sign_in_at ?? null,
lastActiveAt: user.last_active_at ?? null,
createdAt: user.created_at,
updatedAt: user.updated_at,
publicMetadata: user.public_metadata ?? {},
privateMetadata: user.private_metadata ?? {},
unsafeMetadata: user.unsafe_metadata ?? {},
success: true,
},
}
},
outputs: {
id: { type: 'string', description: 'User ID' },
username: { type: 'string', description: 'Username', optional: true },
firstName: { type: 'string', description: 'First name', optional: true },
lastName: { type: 'string', description: 'Last name', optional: true },
imageUrl: { type: 'string', description: 'Profile image URL', optional: true },
hasImage: { type: 'boolean', description: 'Whether user has a profile image' },
primaryEmailAddressId: {
type: 'string',
description: 'Primary email address ID',
optional: true,
},
primaryPhoneNumberId: {
type: 'string',
description: 'Primary phone number ID',
optional: true,
},
primaryWeb3WalletId: { type: 'string', description: 'Primary Web3 wallet ID', optional: true },
emailAddresses: {
type: 'array',
description: 'User email addresses',
items: {
type: 'object',
properties: {
id: { type: 'string', description: 'Email address ID' },
emailAddress: { type: 'string', description: 'Email address' },
verified: { type: 'boolean', description: 'Whether email is verified' },
},
},
},
phoneNumbers: {
type: 'array',
description: 'User phone numbers',
items: {
type: 'object',
properties: {
id: { type: 'string', description: 'Phone number ID' },
phoneNumber: { type: 'string', description: 'Phone number' },
verified: { type: 'boolean', description: 'Whether phone is verified' },
},
},
},
externalId: { type: 'string', description: 'External system ID', optional: true },
passwordEnabled: { type: 'boolean', description: 'Whether password is enabled' },
twoFactorEnabled: { type: 'boolean', description: 'Whether 2FA is enabled' },
totpEnabled: { type: 'boolean', description: 'Whether TOTP is enabled' },
backupCodeEnabled: { type: 'boolean', description: 'Whether backup codes are enabled' },
banned: { type: 'boolean', description: 'Whether user is banned' },
locked: { type: 'boolean', description: 'Whether user is locked' },
deleteSelfEnabled: { type: 'boolean', description: 'Whether user can delete themselves' },
createOrganizationEnabled: {
type: 'boolean',
description: 'Whether user can create organizations',
},
lastSignInAt: { type: 'number', description: 'Last sign-in timestamp', optional: true },
lastActiveAt: { type: 'number', description: 'Last activity timestamp', optional: true },
createdAt: { type: 'number', description: 'Creation timestamp' },
updatedAt: { type: 'number', description: 'Last update timestamp' },
publicMetadata: { type: 'json', description: 'Public metadata (readable from frontend)' },
privateMetadata: { type: 'json', description: 'Private metadata (backend only)' },
unsafeMetadata: { type: 'json', description: 'Unsafe metadata (modifiable from frontend)' },
success: { type: 'boolean', description: 'Operation success status' },
},
}

View File

@@ -0,0 +1,11 @@
export { clerkCreateOrganizationTool } from './create_organization'
export { clerkCreateUserTool } from './create_user'
export { clerkDeleteUserTool } from './delete_user'
export { clerkGetOrganizationTool } from './get_organization'
export { clerkGetSessionTool } from './get_session'
export { clerkGetUserTool } from './get_user'
export { clerkListOrganizationsTool } from './list_organizations'
export { clerkListSessionsTool } from './list_sessions'
export { clerkListUsersTool } from './list_users'
export { clerkRevokeSessionTool } from './revoke_session'
export { clerkUpdateUserTool } from './update_user'

View File

@@ -0,0 +1,157 @@
import { createLogger } from '@sim/logger'
import type {
ClerkApiError,
ClerkListOrganizationsParams,
ClerkListOrganizationsResponse,
ClerkOrganization,
} from '@/tools/clerk/types'
import type { ToolConfig } from '@/tools/types'
const logger = createLogger('ClerkListOrganizations')
export const clerkListOrganizationsTool: ToolConfig<
ClerkListOrganizationsParams,
ClerkListOrganizationsResponse
> = {
id: 'clerk_list_organizations',
name: 'List Organizations from Clerk',
description: 'List all organizations in your Clerk application with optional filtering',
version: '1.0.0',
params: {
secretKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'The Clerk Secret Key for API authentication',
},
limit: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description: 'Number of results per page (1-500, default: 10)',
},
offset: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description: 'Number of results to skip for pagination',
},
includeMembersCount: {
type: 'boolean',
required: false,
visibility: 'user-or-llm',
description: 'Include member count for each organization',
},
query: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Search by organization ID, name, or slug',
},
orderBy: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Sort field (name, created_at, members_count) with +/- prefix',
},
},
request: {
url: (params) => {
const queryParams = new URLSearchParams()
if (params.limit) queryParams.append('limit', params.limit.toString())
if (params.offset) queryParams.append('offset', params.offset.toString())
if (params.includeMembersCount) queryParams.append('include_members_count', 'true')
if (params.query) queryParams.append('query', params.query)
if (params.orderBy) queryParams.append('order_by', params.orderBy)
const queryString = queryParams.toString()
return queryString
? `https://api.clerk.com/v1/organizations?${queryString}`
: 'https://api.clerk.com/v1/organizations'
},
method: 'GET',
headers: (params) => {
if (!params.secretKey) {
throw new Error('Clerk Secret Key is required')
}
return {
Authorization: `Bearer ${params.secretKey}`,
'Content-Type': 'application/json',
}
},
},
transformResponse: async (response: Response) => {
const json: { data: ClerkOrganization[]; total_count: number } | ClerkApiError =
await response.json()
if (!response.ok) {
logger.error('Clerk API request failed', { data: json, status: response.status })
throw new Error(
(json as ClerkApiError).errors?.[0]?.message || 'Failed to list organizations from Clerk'
)
}
const responseData = json as { data: ClerkOrganization[]; total_count: number }
// Transform each organization to extract key fields
const organizations = responseData.data.map((org) => ({
id: org.id,
name: org.name,
slug: org.slug ?? null,
imageUrl: org.image_url ?? null,
hasImage: org.has_image ?? false,
membersCount: org.members_count ?? null,
pendingInvitationsCount: org.pending_invitations_count ?? null,
maxAllowedMemberships: org.max_allowed_memberships ?? 0,
adminDeleteEnabled: org.admin_delete_enabled ?? false,
createdBy: org.created_by ?? null,
createdAt: org.created_at,
updatedAt: org.updated_at,
publicMetadata: org.public_metadata ?? {},
}))
return {
success: true,
output: {
organizations,
totalCount: responseData.total_count ?? organizations.length,
success: true,
},
}
},
outputs: {
organizations: {
type: 'array',
description: 'Array of Clerk organization objects',
items: {
type: 'object',
properties: {
id: { type: 'string', description: 'Organization ID' },
name: { type: 'string', description: 'Organization name' },
slug: { type: 'string', description: 'Organization slug', optional: true },
imageUrl: { type: 'string', description: 'Organization image URL', optional: true },
hasImage: { type: 'boolean', description: 'Whether organization has an image' },
membersCount: { type: 'number', description: 'Number of members', optional: true },
pendingInvitationsCount: {
type: 'number',
description: 'Number of pending invitations',
optional: true,
},
maxAllowedMemberships: { type: 'number', description: 'Max allowed memberships' },
adminDeleteEnabled: { type: 'boolean', description: 'Whether admin delete is enabled' },
createdBy: { type: 'string', description: 'Creator user ID', optional: true },
createdAt: { type: 'number', description: 'Creation timestamp' },
updatedAt: { type: 'number', description: 'Last update timestamp' },
publicMetadata: { type: 'json', description: 'Public metadata' },
},
},
},
totalCount: { type: 'number', description: 'Total number of organizations' },
success: { type: 'boolean', description: 'Operation success status' },
},
}

View File

@@ -0,0 +1,152 @@
import { createLogger } from '@sim/logger'
import type {
ClerkApiError,
ClerkListSessionsParams,
ClerkListSessionsResponse,
ClerkSession,
} from '@/tools/clerk/types'
import type { ToolConfig } from '@/tools/types'
const logger = createLogger('ClerkListSessions')
export const clerkListSessionsTool: ToolConfig<ClerkListSessionsParams, ClerkListSessionsResponse> =
{
id: 'clerk_list_sessions',
name: 'List Sessions from Clerk',
description: 'List sessions for a user or client in your Clerk application',
version: '1.0.0',
params: {
secretKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'The Clerk Secret Key for API authentication',
},
userId: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'User ID to list sessions for (required if clientId not provided)',
},
clientId: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Client ID to list sessions for (required if userId not provided)',
},
status: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description:
'Filter by session status (abandoned, active, ended, expired, pending, removed, replaced, revoked)',
},
limit: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description: 'Number of results per page (1-500, default: 10)',
},
offset: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description: 'Number of results to skip for pagination',
},
},
request: {
url: (params) => {
const queryParams = new URLSearchParams()
if (params.userId) queryParams.append('user_id', params.userId)
if (params.clientId) queryParams.append('client_id', params.clientId)
if (params.status) queryParams.append('status', params.status)
if (params.limit) queryParams.append('limit', params.limit.toString())
if (params.offset) queryParams.append('offset', params.offset.toString())
const queryString = queryParams.toString()
return queryString
? `https://api.clerk.com/v1/sessions?${queryString}`
: 'https://api.clerk.com/v1/sessions'
},
method: 'GET',
headers: (params) => {
if (!params.secretKey) {
throw new Error('Clerk Secret Key is required')
}
return {
Authorization: `Bearer ${params.secretKey}`,
'Content-Type': 'application/json',
}
},
},
transformResponse: async (response: Response) => {
const data: ClerkSession[] | ClerkApiError = await response.json()
if (!response.ok) {
logger.error('Clerk API request failed', { data, status: response.status })
throw new Error(
(data as ClerkApiError).errors?.[0]?.message || 'Failed to list sessions from Clerk'
)
}
const totalCount = Number.parseInt(response.headers.get('x-total-count') || '0', 10)
const sessions = (data as ClerkSession[]).map((session) => ({
id: session.id,
userId: session.user_id,
clientId: session.client_id,
status: session.status,
lastActiveAt: session.last_active_at ?? null,
lastActiveOrganizationId: session.last_active_organization_id ?? null,
expireAt: session.expire_at ?? null,
abandonAt: session.abandon_at ?? null,
createdAt: session.created_at,
updatedAt: session.updated_at,
}))
return {
success: true,
output: {
sessions,
totalCount: totalCount || sessions.length,
success: true,
},
}
},
outputs: {
sessions: {
type: 'array',
description: 'Array of Clerk session objects',
items: {
type: 'object',
properties: {
id: { type: 'string', description: 'Session ID' },
userId: { type: 'string', description: 'User ID' },
clientId: { type: 'string', description: 'Client ID' },
status: { type: 'string', description: 'Session status' },
lastActiveAt: {
type: 'number',
description: 'Last activity timestamp',
optional: true,
},
lastActiveOrganizationId: {
type: 'string',
description: 'Last active organization ID',
optional: true,
},
expireAt: { type: 'number', description: 'Expiration timestamp', optional: true },
abandonAt: { type: 'number', description: 'Abandon timestamp', optional: true },
createdAt: { type: 'number', description: 'Creation timestamp' },
updatedAt: { type: 'number', description: 'Last update timestamp' },
},
},
},
totalCount: { type: 'number', description: 'Total number of sessions' },
success: { type: 'boolean', description: 'Operation success status' },
},
}

View File

@@ -0,0 +1,250 @@
import { createLogger } from '@sim/logger'
import type {
ClerkApiError,
ClerkEmailAddress,
ClerkListUsersParams,
ClerkListUsersResponse,
ClerkPhoneNumber,
ClerkUser,
} from '@/tools/clerk/types'
import type { ToolConfig } from '@/tools/types'
const logger = createLogger('ClerkListUsers')
export const clerkListUsersTool: ToolConfig<ClerkListUsersParams, ClerkListUsersResponse> = {
id: 'clerk_list_users',
name: 'List Users from Clerk',
description: 'List all users in your Clerk application with optional filtering and pagination',
version: '1.0.0',
params: {
secretKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'The Clerk Secret Key for API authentication',
},
limit: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description: 'Number of results per page (1-500, default: 10)',
},
offset: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description: 'Number of results to skip for pagination',
},
orderBy: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Sort field with optional +/- prefix for direction (default: -created_at)',
},
emailAddress: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Filter by email address (comma-separated for multiple)',
},
phoneNumber: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Filter by phone number (comma-separated for multiple)',
},
externalId: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Filter by external ID (comma-separated for multiple)',
},
username: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Filter by username (comma-separated for multiple)',
},
userId: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Filter by user ID (comma-separated for multiple)',
},
query: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Search query to match across email, phone, username, and names',
},
},
request: {
url: (params) => {
const queryParams = new URLSearchParams()
if (params.limit) queryParams.append('limit', params.limit.toString())
if (params.offset) queryParams.append('offset', params.offset.toString())
if (params.orderBy) queryParams.append('order_by', params.orderBy)
if (params.query) queryParams.append('query', params.query)
// Handle comma-separated array params
if (params.emailAddress) {
params.emailAddress.split(',').forEach((email) => {
queryParams.append('email_address', email.trim())
})
}
if (params.phoneNumber) {
params.phoneNumber.split(',').forEach((phone) => {
queryParams.append('phone_number', phone.trim())
})
}
if (params.externalId) {
params.externalId.split(',').forEach((id) => {
queryParams.append('external_id', id.trim())
})
}
if (params.username) {
params.username.split(',').forEach((uname) => {
queryParams.append('username', uname.trim())
})
}
if (params.userId) {
params.userId.split(',').forEach((id) => {
queryParams.append('user_id', id.trim())
})
}
const queryString = queryParams.toString()
return queryString
? `https://api.clerk.com/v1/users?${queryString}`
: 'https://api.clerk.com/v1/users'
},
method: 'GET',
headers: (params) => {
if (!params.secretKey) {
throw new Error('Clerk Secret Key is required')
}
return {
Authorization: `Bearer ${params.secretKey}`,
'Content-Type': 'application/json',
}
},
},
transformResponse: async (response: Response) => {
const data: ClerkUser[] | ClerkApiError = await response.json()
if (!response.ok) {
logger.error('Clerk API request failed', { data, status: response.status })
throw new Error(
(data as ClerkApiError).errors?.[0]?.message || 'Failed to list users from Clerk'
)
}
// The response is an array of users, total_count is in the header
const totalCount = Number.parseInt(response.headers.get('x-total-count') || '0', 10)
// Transform each user to extract key fields
const users = (data as ClerkUser[]).map((user) => ({
id: user.id,
username: user.username ?? null,
firstName: user.first_name ?? null,
lastName: user.last_name ?? null,
imageUrl: user.image_url ?? null,
hasImage: user.has_image ?? false,
primaryEmailAddressId: user.primary_email_address_id ?? null,
primaryPhoneNumberId: user.primary_phone_number_id ?? null,
emailAddresses: (user.email_addresses ?? []).map((email: ClerkEmailAddress) => ({
id: email.id,
emailAddress: email.email_address,
})),
phoneNumbers: (user.phone_numbers ?? []).map((phone: ClerkPhoneNumber) => ({
id: phone.id,
phoneNumber: phone.phone_number,
})),
externalId: user.external_id ?? null,
passwordEnabled: user.password_enabled ?? false,
twoFactorEnabled: user.two_factor_enabled ?? false,
banned: user.banned ?? false,
locked: user.locked ?? false,
lastSignInAt: user.last_sign_in_at ?? null,
lastActiveAt: user.last_active_at ?? null,
createdAt: user.created_at,
updatedAt: user.updated_at,
publicMetadata: user.public_metadata ?? {},
}))
return {
success: true,
output: {
users,
totalCount: totalCount || users.length,
success: true,
},
}
},
outputs: {
users: {
type: 'array',
description: 'Array of Clerk user objects',
items: {
type: 'object',
properties: {
id: { type: 'string', description: 'User ID' },
username: { type: 'string', description: 'Username', optional: true },
firstName: { type: 'string', description: 'First name', optional: true },
lastName: { type: 'string', description: 'Last name', optional: true },
imageUrl: { type: 'string', description: 'Profile image URL', optional: true },
hasImage: { type: 'boolean', description: 'Whether user has a profile image' },
primaryEmailAddressId: {
type: 'string',
description: 'Primary email address ID',
optional: true,
},
primaryPhoneNumberId: {
type: 'string',
description: 'Primary phone number ID',
optional: true,
},
emailAddresses: {
type: 'array',
description: 'User email addresses',
items: {
type: 'object',
properties: {
id: { type: 'string', description: 'Email address ID' },
emailAddress: { type: 'string', description: 'Email address' },
},
},
},
phoneNumbers: {
type: 'array',
description: 'User phone numbers',
items: {
type: 'object',
properties: {
id: { type: 'string', description: 'Phone number ID' },
phoneNumber: { type: 'string', description: 'Phone number' },
},
},
},
externalId: { type: 'string', description: 'External system ID', optional: true },
passwordEnabled: { type: 'boolean', description: 'Whether password is enabled' },
twoFactorEnabled: { type: 'boolean', description: 'Whether 2FA is enabled' },
banned: { type: 'boolean', description: 'Whether user is banned' },
locked: { type: 'boolean', description: 'Whether user is locked' },
lastSignInAt: { type: 'number', description: 'Last sign-in timestamp', optional: true },
lastActiveAt: { type: 'number', description: 'Last activity timestamp', optional: true },
createdAt: { type: 'number', description: 'Creation timestamp' },
updatedAt: { type: 'number', description: 'Last update timestamp' },
publicMetadata: { type: 'json', description: 'Public metadata' },
},
},
},
totalCount: { type: 'number', description: 'Total number of users matching the query' },
success: { type: 'boolean', description: 'Operation success status' },
},
}

View File

@@ -0,0 +1,96 @@
import { createLogger } from '@sim/logger'
import type {
ClerkApiError,
ClerkRevokeSessionParams,
ClerkRevokeSessionResponse,
ClerkSession,
} from '@/tools/clerk/types'
import type { ToolConfig } from '@/tools/types'
const logger = createLogger('ClerkRevokeSession')
export const clerkRevokeSessionTool: ToolConfig<
ClerkRevokeSessionParams,
ClerkRevokeSessionResponse
> = {
id: 'clerk_revoke_session',
name: 'Revoke Session in Clerk',
description: 'Revoke a session to immediately invalidate it',
version: '1.0.0',
params: {
secretKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'The Clerk Secret Key for API authentication',
},
sessionId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'The ID of the session to revoke',
},
},
request: {
url: (params) => `https://api.clerk.com/v1/sessions/${params.sessionId}/revoke`,
method: 'POST',
headers: (params) => {
if (!params.secretKey) {
throw new Error('Clerk Secret Key is required')
}
return {
Authorization: `Bearer ${params.secretKey}`,
'Content-Type': 'application/json',
}
},
},
transformResponse: async (response: Response) => {
const data: ClerkSession | ClerkApiError = await response.json()
if (!response.ok) {
logger.error('Clerk API request failed', { data, status: response.status })
throw new Error(
(data as ClerkApiError).errors?.[0]?.message || 'Failed to revoke session in Clerk'
)
}
const session = data as ClerkSession
return {
success: true,
output: {
id: session.id,
userId: session.user_id,
clientId: session.client_id,
status: session.status,
lastActiveAt: session.last_active_at ?? null,
lastActiveOrganizationId: session.last_active_organization_id ?? null,
expireAt: session.expire_at ?? null,
abandonAt: session.abandon_at ?? null,
createdAt: session.created_at,
updatedAt: session.updated_at,
success: true,
},
}
},
outputs: {
id: { type: 'string', description: 'Session ID' },
userId: { type: 'string', description: 'User ID' },
clientId: { type: 'string', description: 'Client ID' },
status: { type: 'string', description: 'Session status (should be revoked)' },
lastActiveAt: { type: 'number', description: 'Last activity timestamp', optional: true },
lastActiveOrganizationId: {
type: 'string',
description: 'Last active organization ID',
optional: true,
},
expireAt: { type: 'number', description: 'Expiration timestamp', optional: true },
abandonAt: { type: 'number', description: 'Abandon timestamp', optional: true },
createdAt: { type: 'number', description: 'Creation timestamp' },
updatedAt: { type: 'number', description: 'Last update timestamp' },
success: { type: 'boolean', description: 'Operation success status' },
},
}

View File

@@ -0,0 +1,552 @@
import type { ToolResponse } from '@/tools/types'
/**
* Clerk API error response
*/
export interface ClerkApiError {
errors?: { message: string }[]
}
/**
* Clerk delete response
*/
export interface ClerkDeleteResponse {
id: string
object: string
deleted: boolean
}
/**
* Clerk User object
*/
export interface ClerkUser {
id: string
object: 'user'
username: string | null
first_name: string | null
last_name: string | null
image_url: string
has_image: boolean
primary_email_address_id: string | null
primary_phone_number_id: string | null
primary_web3_wallet_id: string | null
password_enabled: boolean
two_factor_enabled: boolean
totp_enabled: boolean
backup_code_enabled: boolean
email_addresses: ClerkEmailAddress[]
phone_numbers: ClerkPhoneNumber[]
web3_wallets: ClerkWeb3Wallet[]
external_accounts: ClerkExternalAccount[]
external_id: string | null
last_sign_in_at: number | null
banned: boolean
locked: boolean
lockout_expires_in_seconds: number | null
verification_attempts_remaining: number | null
created_at: number
updated_at: number
delete_self_enabled: boolean
create_organization_enabled: boolean
last_active_at: number | null
profile_image_url: string
public_metadata: Record<string, unknown>
private_metadata: Record<string, unknown>
unsafe_metadata: Record<string, unknown>
}
export interface ClerkEmailAddress {
id: string
object: 'email_address'
email_address: string
verification: ClerkVerification | null
linked_to: ClerkLinkedIdentifier[]
created_at: number
updated_at: number
}
export interface ClerkPhoneNumber {
id: string
object: 'phone_number'
phone_number: string
reserved_for_second_factor: boolean
default_second_factor: boolean
verification: ClerkVerification | null
linked_to: ClerkLinkedIdentifier[]
backup_codes: string[] | null
created_at: number
updated_at: number
}
export interface ClerkWeb3Wallet {
id: string
object: 'web3_wallet'
web3_wallet: string
verification: ClerkVerification | null
created_at: number
updated_at: number
}
export interface ClerkExternalAccount {
id: string
object: 'external_account'
provider: string
identification_id: string
provider_user_id: string
approved_scopes: string
email_address: string
first_name: string
last_name: string
image_url: string
username: string | null
public_metadata: Record<string, unknown>
label: string | null
verification: ClerkVerification | null
created_at: number
updated_at: number
}
export interface ClerkVerification {
status: string
strategy: string
attempts: number | null
expire_at: number | null
}
export interface ClerkLinkedIdentifier {
type: string
id: string
}
/**
* Clerk Organization object
*/
export interface ClerkOrganization {
id: string
object: 'organization'
name: string
slug: string
image_url: string
has_image: boolean
members_count?: number
pending_invitations_count?: number
max_allowed_memberships: number
admin_delete_enabled: boolean
public_metadata: Record<string, unknown>
private_metadata: Record<string, unknown>
created_by: string
created_at: number
updated_at: number
}
/**
* Clerk Session object
*/
export interface ClerkSession {
id: string
object: 'session'
user_id: string
client_id: string
actor: Record<string, unknown> | null
status:
| 'abandoned'
| 'active'
| 'ended'
| 'expired'
| 'pending'
| 'removed'
| 'replaced'
| 'revoked'
last_active_organization_id: string | null
last_active_at: number
expire_at: number
abandon_at: number
created_at: number
updated_at: number
}
/**
* Transformed email address for outputs
*/
export interface ClerkEmailAddressOutput {
id: string
emailAddress: string
verified?: boolean
}
/**
* Transformed phone number for outputs
*/
export interface ClerkPhoneNumberOutput {
id: string
phoneNumber: string
verified?: boolean
}
/**
* Transformed user for list outputs
*/
export interface ClerkUserOutput {
id: string
username: string | null
firstName: string | null
lastName: string | null
imageUrl: string | null
hasImage: boolean
primaryEmailAddressId: string | null
primaryPhoneNumberId: string | null
emailAddresses: ClerkEmailAddressOutput[]
phoneNumbers: ClerkPhoneNumberOutput[]
externalId: string | null
passwordEnabled: boolean
twoFactorEnabled: boolean
banned: boolean
locked: boolean
lastSignInAt: number | null
lastActiveAt: number | null
createdAt: number
updatedAt: number
publicMetadata: Record<string, unknown>
}
/**
* Transformed organization for outputs
*/
export interface ClerkOrganizationOutput {
id: string
name: string
slug: string | null
imageUrl: string | null
hasImage: boolean
membersCount: number | null
pendingInvitationsCount: number | null
maxAllowedMemberships: number
adminDeleteEnabled: boolean
createdBy: string | null
createdAt: number
updatedAt: number
publicMetadata: Record<string, unknown>
}
/**
* Transformed session for outputs
*/
export interface ClerkSessionOutput {
id: string
userId: string
clientId: string
status: string
lastActiveAt: number | null
lastActiveOrganizationId: string | null
expireAt: number | null
abandonAt: number | null
createdAt: number
updatedAt: number
}
// List Users
export interface ClerkListUsersParams {
secretKey: string
limit?: number
offset?: number
orderBy?: string
emailAddress?: string
phoneNumber?: string
externalId?: string
username?: string
userId?: string
query?: string
}
export interface ClerkListUsersResponse extends ToolResponse {
output: {
users: ClerkUserOutput[]
totalCount: number
success: boolean
}
}
// Get User
export interface ClerkGetUserParams {
secretKey: string
userId: string
}
export interface ClerkGetUserResponse extends ToolResponse {
output: {
id: string
username: string | null
firstName: string | null
lastName: string | null
imageUrl: string | null
hasImage: boolean
primaryEmailAddressId: string | null
primaryPhoneNumberId: string | null
primaryWeb3WalletId: string | null
emailAddresses: ClerkEmailAddressOutput[]
phoneNumbers: ClerkPhoneNumberOutput[]
externalId: string | null
passwordEnabled: boolean
twoFactorEnabled: boolean
totpEnabled: boolean
backupCodeEnabled: boolean
banned: boolean
locked: boolean
deleteSelfEnabled: boolean
createOrganizationEnabled: boolean
lastSignInAt: number | null
lastActiveAt: number | null
createdAt: number
updatedAt: number
publicMetadata: Record<string, unknown>
privateMetadata: Record<string, unknown>
unsafeMetadata: Record<string, unknown>
success: boolean
}
}
// Create User
export interface ClerkCreateUserParams {
secretKey: string
emailAddress?: string | string[]
phoneNumber?: string | string[]
username?: string
password?: string
firstName?: string
lastName?: string
externalId?: string
publicMetadata?: Record<string, unknown>
privateMetadata?: Record<string, unknown>
unsafeMetadata?: Record<string, unknown>
skipPasswordChecks?: boolean
skipPasswordRequirement?: boolean
}
export interface ClerkCreateUserResponse extends ToolResponse {
output: {
id: string
username: string | null
firstName: string | null
lastName: string | null
imageUrl: string | null
primaryEmailAddressId: string | null
primaryPhoneNumberId: string | null
emailAddresses: ClerkEmailAddressOutput[]
phoneNumbers: ClerkPhoneNumberOutput[]
externalId: string | null
createdAt: number
updatedAt: number
publicMetadata: Record<string, unknown>
success: boolean
}
}
// Update User
export interface ClerkUpdateUserParams {
secretKey: string
userId: string
firstName?: string
lastName?: string
username?: string
password?: string
externalId?: string
primaryEmailAddressId?: string
primaryPhoneNumberId?: string
publicMetadata?: Record<string, unknown>
privateMetadata?: Record<string, unknown>
unsafeMetadata?: Record<string, unknown>
skipPasswordChecks?: boolean
}
export interface ClerkUpdateUserResponse extends ToolResponse {
output: {
id: string
username: string | null
firstName: string | null
lastName: string | null
imageUrl: string | null
primaryEmailAddressId: string | null
primaryPhoneNumberId: string | null
emailAddresses: ClerkEmailAddressOutput[]
phoneNumbers: ClerkPhoneNumberOutput[]
externalId: string | null
banned: boolean
locked: boolean
createdAt: number
updatedAt: number
publicMetadata: Record<string, unknown>
success: boolean
}
}
// Delete User
export interface ClerkDeleteUserParams {
secretKey: string
userId: string
}
export interface ClerkDeleteUserResponse extends ToolResponse {
output: {
id: string
object: string
deleted: boolean
success: boolean
}
}
// List Organizations
export interface ClerkListOrganizationsParams {
secretKey: string
limit?: number
offset?: number
includeMembersCount?: boolean
query?: string
orderBy?: string
}
export interface ClerkListOrganizationsResponse extends ToolResponse {
output: {
organizations: ClerkOrganizationOutput[]
totalCount: number
success: boolean
}
}
// Get Organization
export interface ClerkGetOrganizationParams {
secretKey: string
organizationId: string
}
export interface ClerkGetOrganizationResponse extends ToolResponse {
output: {
id: string
name: string
slug: string | null
imageUrl: string | null
hasImage: boolean
membersCount: number | null
pendingInvitationsCount: number | null
maxAllowedMemberships: number
adminDeleteEnabled: boolean
createdBy: string | null
createdAt: number
updatedAt: number
publicMetadata: Record<string, unknown>
success: boolean
}
}
// Create Organization
export interface ClerkCreateOrganizationParams {
secretKey: string
name: string
createdBy: string
slug?: string
maxAllowedMemberships?: number
publicMetadata?: Record<string, unknown>
privateMetadata?: Record<string, unknown>
}
export interface ClerkCreateOrganizationResponse extends ToolResponse {
output: {
id: string
name: string
slug: string | null
imageUrl: string | null
hasImage: boolean
membersCount: number | null
pendingInvitationsCount: number | null
maxAllowedMemberships: number
adminDeleteEnabled: boolean
createdBy: string | null
createdAt: number
updatedAt: number
publicMetadata: Record<string, unknown>
success: boolean
}
}
// List Sessions
export interface ClerkListSessionsParams {
secretKey: string
userId?: string
clientId?: string
status?:
| 'abandoned'
| 'active'
| 'ended'
| 'expired'
| 'pending'
| 'removed'
| 'replaced'
| 'revoked'
limit?: number
offset?: number
}
export interface ClerkListSessionsResponse extends ToolResponse {
output: {
sessions: ClerkSessionOutput[]
totalCount: number
success: boolean
}
}
// Get Session
export interface ClerkGetSessionParams {
secretKey: string
sessionId: string
}
export interface ClerkGetSessionResponse extends ToolResponse {
output: {
id: string
userId: string
clientId: string
status: string
lastActiveAt: number | null
lastActiveOrganizationId: string | null
expireAt: number | null
abandonAt: number | null
createdAt: number
updatedAt: number
success: boolean
}
}
// Revoke Session
export interface ClerkRevokeSessionParams {
secretKey: string
sessionId: string
}
export interface ClerkRevokeSessionResponse extends ToolResponse {
output: {
id: string
userId: string
clientId: string
status: string
lastActiveAt: number | null
lastActiveOrganizationId: string | null
expireAt: number | null
abandonAt: number | null
createdAt: number
updatedAt: number
success: boolean
}
}
// Generic response type for the block
export type ClerkResponse =
| ClerkListUsersResponse
| ClerkGetUserResponse
| ClerkCreateUserResponse
| ClerkUpdateUserResponse
| ClerkDeleteUserResponse
| ClerkListOrganizationsResponse
| ClerkGetOrganizationResponse
| ClerkCreateOrganizationResponse
| ClerkListSessionsResponse
| ClerkGetSessionResponse
| ClerkRevokeSessionResponse

View File

@@ -0,0 +1,225 @@
import { createLogger } from '@sim/logger'
import type {
ClerkApiError,
ClerkEmailAddress,
ClerkPhoneNumber,
ClerkUpdateUserParams,
ClerkUpdateUserResponse,
ClerkUser,
} from '@/tools/clerk/types'
import type { ToolConfig } from '@/tools/types'
const logger = createLogger('ClerkUpdateUser')
export const clerkUpdateUserTool: ToolConfig<ClerkUpdateUserParams, ClerkUpdateUserResponse> = {
id: 'clerk_update_user',
name: 'Update User in Clerk',
description: 'Update an existing user in your Clerk application',
version: '1.0.0',
params: {
secretKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'The Clerk Secret Key for API authentication',
},
userId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'The ID of the user to update',
},
firstName: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'First name of the user',
},
lastName: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Last name of the user',
},
username: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Username (must be unique)',
},
password: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'New password (minimum 8 characters)',
},
externalId: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'External system identifier',
},
primaryEmailAddressId: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'ID of verified email to set as primary',
},
primaryPhoneNumberId: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'ID of verified phone to set as primary',
},
publicMetadata: {
type: 'json',
required: false,
visibility: 'user-or-llm',
description: 'Public metadata (JSON object)',
},
privateMetadata: {
type: 'json',
required: false,
visibility: 'user-or-llm',
description: 'Private metadata (JSON object)',
},
unsafeMetadata: {
type: 'json',
required: false,
visibility: 'user-or-llm',
description: 'Unsafe metadata (JSON object)',
},
skipPasswordChecks: {
type: 'boolean',
required: false,
visibility: 'user-or-llm',
description: 'Skip password validation checks',
},
},
request: {
url: (params) => `https://api.clerk.com/v1/users/${params.userId?.trim()}`,
method: 'PATCH',
headers: (params) => {
if (!params.secretKey) {
throw new Error('Clerk Secret Key is required')
}
return {
Authorization: `Bearer ${params.secretKey}`,
'Content-Type': 'application/json',
}
},
body: (params) => {
const body: Record<string, unknown> = {}
if (params.firstName !== undefined) body.first_name = params.firstName?.trim()
if (params.lastName !== undefined) body.last_name = params.lastName?.trim()
if (params.username !== undefined) body.username = params.username?.trim()
if (params.password !== undefined) body.password = params.password
if (params.externalId !== undefined) body.external_id = params.externalId?.trim()
if (params.primaryEmailAddressId !== undefined)
body.primary_email_address_id = params.primaryEmailAddressId?.trim()
if (params.primaryPhoneNumberId !== undefined)
body.primary_phone_number_id = params.primaryPhoneNumberId?.trim()
if (params.publicMetadata !== undefined) body.public_metadata = params.publicMetadata
if (params.privateMetadata !== undefined) body.private_metadata = params.privateMetadata
if (params.unsafeMetadata !== undefined) body.unsafe_metadata = params.unsafeMetadata
if (params.skipPasswordChecks !== undefined)
body.skip_password_checks = params.skipPasswordChecks
return body
},
},
transformResponse: async (response: Response) => {
const data: ClerkUser | ClerkApiError = await response.json()
if (!response.ok) {
logger.error('Clerk API request failed', { data, status: response.status })
throw new Error(
(data as ClerkApiError).errors?.[0]?.message || 'Failed to update user in Clerk'
)
}
const user = data as ClerkUser
return {
success: true,
output: {
id: user.id,
username: user.username ?? null,
firstName: user.first_name ?? null,
lastName: user.last_name ?? null,
imageUrl: user.image_url ?? null,
primaryEmailAddressId: user.primary_email_address_id ?? null,
primaryPhoneNumberId: user.primary_phone_number_id ?? null,
emailAddresses: (user.email_addresses ?? []).map((email: ClerkEmailAddress) => ({
id: email.id,
emailAddress: email.email_address,
verified: email.verification?.status === 'verified',
})),
phoneNumbers: (user.phone_numbers ?? []).map((phone: ClerkPhoneNumber) => ({
id: phone.id,
phoneNumber: phone.phone_number,
verified: phone.verification?.status === 'verified',
})),
externalId: user.external_id ?? null,
banned: user.banned ?? false,
locked: user.locked ?? false,
createdAt: user.created_at,
updatedAt: user.updated_at,
publicMetadata: user.public_metadata ?? {},
success: true,
},
}
},
outputs: {
id: { type: 'string', description: 'Updated user ID' },
username: { type: 'string', description: 'Username', optional: true },
firstName: { type: 'string', description: 'First name', optional: true },
lastName: { type: 'string', description: 'Last name', optional: true },
imageUrl: { type: 'string', description: 'Profile image URL', optional: true },
primaryEmailAddressId: {
type: 'string',
description: 'Primary email address ID',
optional: true,
},
primaryPhoneNumberId: {
type: 'string',
description: 'Primary phone number ID',
optional: true,
},
emailAddresses: {
type: 'array',
description: 'User email addresses',
items: {
type: 'object',
properties: {
id: { type: 'string', description: 'Email address ID' },
emailAddress: { type: 'string', description: 'Email address' },
verified: { type: 'boolean', description: 'Whether email is verified' },
},
},
},
phoneNumbers: {
type: 'array',
description: 'User phone numbers',
items: {
type: 'object',
properties: {
id: { type: 'string', description: 'Phone number ID' },
phoneNumber: { type: 'string', description: 'Phone number' },
verified: { type: 'boolean', description: 'Whether phone is verified' },
},
},
},
externalId: { type: 'string', description: 'External system ID', optional: true },
banned: { type: 'boolean', description: 'Whether user is banned' },
locked: { type: 'boolean', description: 'Whether user is locked' },
createdAt: { type: 'number', description: 'Creation timestamp' },
updatedAt: { type: 'number', description: 'Last update timestamp' },
publicMetadata: { type: 'json', description: 'Public metadata' },
success: { type: 'boolean', description: 'Operation success status' },
},
}

View File

@@ -75,6 +75,19 @@ import {
calendlyListWebhooksTool,
} from '@/tools/calendly'
import { clayPopulateTool } from '@/tools/clay'
import {
clerkCreateOrganizationTool,
clerkCreateUserTool,
clerkDeleteUserTool,
clerkGetOrganizationTool,
clerkGetSessionTool,
clerkGetUserTool,
clerkListOrganizationsTool,
clerkListSessionsTool,
clerkListUsersTool,
clerkRevokeSessionTool,
clerkUpdateUserTool,
} from '@/tools/clerk'
import {
confluenceCreateCommentTool,
confluenceCreatePageTool,
@@ -2571,6 +2584,17 @@ export const tools: Record<string, ToolConfig> = {
telegram_send_video: telegramSendVideoTool,
telegram_send_document: telegramSendDocumentTool,
clay_populate: clayPopulateTool,
clerk_list_users: clerkListUsersTool,
clerk_get_user: clerkGetUserTool,
clerk_create_user: clerkCreateUserTool,
clerk_update_user: clerkUpdateUserTool,
clerk_delete_user: clerkDeleteUserTool,
clerk_list_organizations: clerkListOrganizationsTool,
clerk_get_organization: clerkGetOrganizationTool,
clerk_create_organization: clerkCreateOrganizationTool,
clerk_list_sessions: clerkListSessionsTool,
clerk_get_session: clerkGetSessionTool,
clerk_revoke_session: clerkRevokeSessionTool,
discord_send_message: discordSendMessageTool,
discord_get_messages: discordGetMessagesTool,
discord_get_server: discordGetServerTool,

View File

@@ -1122,6 +1122,15 @@ function parsePropertiesContent(propertiesContent: string): Record<string, any>
continue
}
// Check if this match is at depth 0 (not inside nested braces)
// Only process top-level properties, skip nested ones
const beforeMatch = propertiesContent.substring(0, match.index)
const openBraces = (beforeMatch.match(/{/g) || []).length
const closeBraces = (beforeMatch.match(/}/g) || []).length
if (openBraces !== closeBraces) {
continue // Skip - this is a nested property
}
const startPos = match.index + match[0].length - 1
let braceCount = 1