Compare commits

...

21 Commits

Author SHA1 Message Date
Vikhyath Mondreti
8c0a2e04b1 v0.5.108: workflow input params in agent tools, bun upgrade, dropdown selectors for 14 blocks 2026-03-06 21:02:25 -08:00
Waleed
0a52b09deb feat(jira): add search_users tool for user lookup by email (#3451)
* feat(jira): add search_users tool for user lookup by email

* improvement(jira): reuse shared transformUser utility in search_users

* improvement(jira): add pagination fields to search_users response

* update

* fix(jira): filter falsy entries before transforming search_users results

* fix(jira): add defensive fallback for nullable transformUser in search_users

* fix(jira): align search_users response type with transformUser return type
2026-03-06 19:52:37 -08:00
Vikhyath Mondreti
1d36b80172 improvement(selectors): remove dead semantic fallback code (#3454)
* improvement(selectors): simplify selectorContext + add tests

* fix resolve values fallback

* another workflowid pass through

* remove dead code

* make workspace id required
2026-03-06 19:38:57 -08:00
Vikhyath Mondreti
e6a5e7f4e4 improvement(selectors): simplify selector context + add tests (#3453)
* improvement(selectors): simplify selectorContext + add tests

* fix resolve values fallback

* another workflowid pass through
2026-03-06 18:30:46 -08:00
Waleed
6586c5ce40 v0.5.107: new reddit, slack tools 2026-03-05 22:48:20 -08:00
Vikhyath Mondreti
3ce947566d v0.5.106: condition block and legacy kbs fixes, GPT 5.4 2026-03-05 17:30:05 -08:00
Waleed
70c36cb7aa v0.5.105: slack remove reaction, nested subflow locks fix, servicenow pagination, memory improvements 2026-03-04 22:38:26 -08:00
Waleed
f1ec5fe824 v0.5.104: memory improvements, nested subflows, careers page redirect, brandfetch, google meet 2026-03-03 23:45:29 -08:00
Waleed
e07e3c34cc v0.5.103: memory util instrumentation, API docs, amplitude, google pagespeed insights, pagerduty 2026-03-01 23:27:02 -08:00
Waleed
0d2e6ff31d v0.5.102: new integrations, new tools, ci speedups, memory leak instrumentation 2026-02-28 12:48:10 -08:00
Waleed
4fd0989264 v0.5.101: circular dependency mitigation, confluence enhancements, google tasks and bigquery integrations, workflow lock 2026-02-26 15:04:53 -08:00
Waleed
67f8a687f6 v0.5.100: multiple credentials, 40% speedup, gong, attio, audit log improvements 2026-02-25 00:28:25 -08:00
Waleed
af592349d3 v0.5.99: local dev improvements, live workflow logs in terminal 2026-02-23 00:24:49 -08:00
Waleed
0d86ea01f0 v0.5.98: change detection improvements, rate limit and code execution fixes, removed retired models, hex integration 2026-02-21 18:07:40 -08:00
Waleed
115f04e989 v0.5.97: oidc discovery for copilot mcp 2026-02-21 02:06:25 -08:00
Waleed
34d92fae89 v0.5.96: sim oauth provider, slack ephemeral message tool and blockkit support 2026-02-20 18:22:20 -08:00
Waleed
67aa4bb332 v0.5.95: gemini 3.1 pro, cloudflare, dataverse, revenuecat, redis, upstash, algolia tools; isolated-vm robustness improvements, tables backend (#3271)
* feat(tools): advanced fields for youtube, vercel; added cloudflare and dataverse tools (#3257)

* refactor(vercel): mark optional fields as advanced mode

Move optional/power-user fields behind the advanced toggle:
- List Deployments: project filter, target, state
- Create Deployment: project ID override, redeploy from, target
- List Projects: search
- Create/Update Project: framework, build/output/install commands
- Env Vars: variable type
- Webhooks: project IDs filter
- Checks: path, details URL
- Team Members: role filter
- All operations: team ID scope

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* style(youtube): mark optional params as advanced mode

Hide pagination, sort order, and filter fields behind the advanced
toggle for a cleaner default UX across all YouTube operations.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* added advanced fields for vercel and youtube, added cloudflare and dataverse block

* addded desc for dataverse

* add more tools

* ack comment

* more

* ops

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>

* feat(tables): added tables (#2867)

* updates

* required

* trashy table viewer

* updates

* updates

* filtering ui

* updates

* updates

* updates

* one input mode

* format

* fix lints

* improved errors

* updates

* updates

* chages

* doc strings

* breaking down file

* update comments with ai

* updates

* comments

* changes

* revert

* updates

* dedupe

* updates

* updates

* updates

* refactoring

* renames & refactors

* refactoring

* updates

* undo

* update db

* wand

* updates

* fix comments

* fixes

* simplify comments

* u[dates

* renames

* better comments

* validation

* updates

* updates

* updates

* fix sorting

* fix appearnce

* updating prompt to make it user sort

* rm

* updates

* rename

* comments

* clean comments

* simplicifcaiton

* updates

* updates

* refactor

* reduced type confusion

* undo

* rename

* undo changes

* undo

* simplify

* updates

* updates

* revert

* updates

* db updates

* type fix

* fix

* fix error handling

* updates

* docs

* docs

* updates

* rename

* dedupe

* revert

* uncook

* updates

* fix

* fix

* fix

* fix

* prepare merge

* readd migrations

* add back missed code

* migrate enrichment logic to general abstraction

* address bugbot concerns

* adhere to size limits for tables

* remove conflicting migration

* add back migrations

* fix tables auth

* fix permissive auth

* fix lint

* reran migrations

* migrate to use tanstack query for all server state

* update table-selector

* update names

* added tables to permission groups, updated subblock types

---------

Co-authored-by: Vikhyath Mondreti <vikhyath@simstudio.ai>
Co-authored-by: waleed <walif6@gmail.com>

* fix(snapshot): changed insert to upsert when concurrent identical child workflows are running (#3259)

* fix(snapshot): changed insert to upsert when concurrent identical child workflows are running

* fixed ci tests failing

* fix(workflows): disallow duplicate workflow names at the same folder level (#3260)

* feat(tools): added redis, upstash, algolia, and revenuecat (#3261)

* feat(tools): added redis, upstash, algolia, and revenuecat

* ack comment

* feat(models): add gemini-3.1-pro-preview and update gemini-3-pro thinking levels (#3263)

* fix(audit-log): lazily resolve actor name/email when missing (#3262)

* fix(blocks): move type coercions from tools.config.tool to tools.config.params (#3264)

* fix(blocks): move type coercions from tools.config.tool to tools.config.params

Number() coercions in tools.config.tool ran at serialization time before
variable resolution, destroying dynamic references like <block.result.count>
by converting them to NaN/null. Moved all coercions to tools.config.params
which runs at execution time after variables are resolved.

Fixed in 15 blocks: exa, arxiv, sentry, incidentio, wikipedia, ahrefs,
posthog, elasticsearch, dropbox, hunter, lemlist, spotify, youtube, grafana,
parallel. Also added mode: 'advanced' to optional exa fields.

Closes #3258

* fix(blocks): address PR review — move remaining param mutations from tool() to params()

- Moved field mappings from tool() to params() in grafana, posthog,
  lemlist, spotify, dropbox (same dynamic reference bug)
- Fixed parallel.ts excerpts/full_content boolean logic
- Fixed parallel.ts search_queries empty case (must set undefined)
- Fixed elasticsearch.ts timeout not included when already ends with 's'
- Restored dropbox.ts tool() switch for proper default fallback

* fix(blocks): restore field renames to tool() for serialization-time validation

Field renames (e.g. personalApiKey→apiKey) must be in tool() because
validateRequiredFieldsBeforeExecution calls selectToolId()→tool() then
checks renamed field names on params. Only type coercions (Number(),
boolean) stay in params() to avoid destroying dynamic variable references.

* improvement(resolver): resovled empty sentinel to not pass through unexecuted valid refs to text inputs (#3266)

* fix(blocks): add required constraint for serviceDeskId in JSM block (#3268)

* fix(blocks): add required constraint for serviceDeskId in JSM block

* fix(blocks): rename custom field values to request field values in JSM create request

* fix(trigger): add isolated-vm support to trigger.dev container builds (#3269)

Scheduled workflow executions running in trigger.dev containers were
failing to spawn isolated-vm workers because the native module wasn't
available in the container. This caused loop condition evaluation to
silently fail and exit after one iteration.

- Add isolated-vm to build.external and additionalPackages in trigger config
- Include isolated-vm-worker.cjs via additionalFiles for child process spawning
- Add fallback path resolution for worker file in trigger.dev environment

* fix(tables): hide tables from sidebar and block registry (#3270)

* fix(tables): hide tables from sidebar and block registry

* fix(trigger): add isolated-vm support to trigger.dev container builds (#3269)

Scheduled workflow executions running in trigger.dev containers were
failing to spawn isolated-vm workers because the native module wasn't
available in the container. This caused loop condition evaluation to
silently fail and exit after one iteration.

- Add isolated-vm to build.external and additionalPackages in trigger config
- Include isolated-vm-worker.cjs via additionalFiles for child process spawning
- Add fallback path resolution for worker file in trigger.dev environment

* lint

* fix(trigger): update node version to align with main app (#3272)

* fix(build): fix corrupted sticky disk cache on blacksmith (#3273)

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: Lakee Sivaraya <71339072+lakeesiv@users.noreply.github.com>
Co-authored-by: Vikhyath Mondreti <vikhyath@simstudio.ai>
Co-authored-by: Vikhyath Mondreti <vikhyathvikku@gmail.com>
2026-02-20 13:43:07 -08:00
Waleed
15ace5e63f v0.5.94: vercel integration, folder insertion, migrated tracking redirects to rewrites 2026-02-18 16:53:34 -08:00
Waleed
fdca73679d v0.5.93: NextJS config changes, MCP and Blocks whitelisting, copilot keyboard shortcuts, audit logs 2026-02-18 12:10:05 -08:00
Waleed
da46a387c9 v0.5.92: shortlinks, copilot scrolling stickiness, pagination 2026-02-17 15:13:21 -08:00
Waleed
b7e377ec4b v0.5.91: docs i18n, turborepo upgrade 2026-02-16 00:36:05 -08:00
17 changed files with 613 additions and 311 deletions

View File

@@ -1014,4 +1014,36 @@ Get Jira users. If an account ID is provided, returns a single user. Otherwise,
| `startAt` | number | Pagination start index |
| `maxResults` | number | Maximum results per page |
### `jira_search_users`
Search for Jira users by email address or display name. Returns matching users with their accountId, displayName, and emailAddress.
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `domain` | string | Yes | Your Jira domain \(e.g., yourcompany.atlassian.net\) |
| `query` | string | Yes | A query string to search for users. Can be an email address, display name, or partial match. |
| `maxResults` | number | No | Maximum number of users to return \(default: 50, max: 1000\) |
| `startAt` | number | No | The index of the first user to return \(for pagination, default: 0\) |
| `cloudId` | string | No | Jira Cloud ID for the instance. If not provided, it will be fetched using the domain. |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `ts` | string | ISO 8601 timestamp of the operation |
| `users` | array | Array of matching Jira users |
| ↳ `accountId` | string | Atlassian account ID of the user |
| ↳ `displayName` | string | Display name of the user |
| ↳ `active` | boolean | Whether the user account is active |
| ↳ `emailAddress` | string | Email address of the user |
| ↳ `accountType` | string | Type of account \(e.g., atlassian, app, customer\) |
| ↳ `avatarUrl` | string | URL to the user avatar \(48x48\) |
| ↳ `timeZone` | string | User timezone |
| ↳ `self` | string | REST API URL for this user |
| `total` | number | Number of users returned in this page \(may be less than total matches\) |
| `startAt` | number | Pagination start index |
| `maxResults` | number | Maximum results per page |

View File

@@ -2,6 +2,7 @@
import { useMemo } from 'react'
import { useParams } from 'next/navigation'
import { SELECTOR_CONTEXT_FIELDS } from '@/lib/workflows/subblocks/context'
import type { SubBlockConfig } from '@/blocks/types'
import { extractEnvVarName, isEnvVarReference, isReference } from '@/executor/constants'
import type { SelectorContext, SelectorKey } from '@/hooks/selectors/types'
@@ -14,8 +15,7 @@ import { useDependsOnGate } from './use-depends-on-gate'
*
* Builds a `SelectorContext` by mapping each `dependsOn` entry through the
* canonical index to its `canonicalParamId`, which maps directly to
* `SelectorContext` field names (e.g. `siteId`, `teamId`, `collectionId`).
* The one special case is `oauthCredential` which maps to `credentialId`.
* `SelectorContext` field names (e.g. `siteId`, `teamId`, `oauthCredential`).
*
* @param blockId - The block containing the selector sub-block
* @param subBlock - The sub-block config (must have `selectorKey` set)
@@ -70,11 +70,8 @@ export function useSelectorSetup(
if (isReference(strValue)) continue
const canonicalParamId = canonicalIndex.canonicalIdBySubBlockId[depKey] ?? depKey
if (canonicalParamId === 'oauthCredential') {
context.credentialId = strValue
} else if (canonicalParamId in CONTEXT_FIELD_SET) {
;(context as Record<string, unknown>)[canonicalParamId] = strValue
if (SELECTOR_CONTEXT_FIELDS.has(canonicalParamId as keyof SelectorContext)) {
context[canonicalParamId as keyof SelectorContext] = strValue
}
}
@@ -89,19 +86,3 @@ export function useSelectorSetup(
dependencyValues: resolvedDependencyValues,
}
}
const CONTEXT_FIELD_SET: Record<string, true> = {
credentialId: true,
domain: true,
teamId: true,
projectId: true,
knowledgeBaseId: true,
planId: true,
siteId: true,
collectionId: true,
spreadsheetId: true,
fileId: true,
baseId: true,
datasetId: true,
serviceDeskId: true,
}

View File

@@ -57,9 +57,9 @@ import { useWebhookManagement } from '@/hooks/use-webhook-management'
const SLACK_OVERRIDES: SelectorOverrides = {
transformContext: (context, deps) => {
const authMethod = deps.authMethod as string
const credentialId =
const oauthCredential =
authMethod === 'bot_token' ? String(deps.botToken ?? '') : String(deps.credential ?? '')
return { ...context, credentialId }
return { ...context, oauthCredential }
},
}

View File

@@ -578,7 +578,7 @@ const SubBlockRow = memo(function SubBlockRow({
subBlock,
value: rawValue,
workflowId,
credentialId: typeof credentialId === 'string' ? credentialId : undefined,
oauthCredential: typeof credentialId === 'string' ? credentialId : undefined,
knowledgeBaseId: typeof knowledgeBaseId === 'string' ? knowledgeBaseId : undefined,
domain: domainValue,
teamId: teamIdValue,

View File

@@ -47,6 +47,7 @@ export const JiraBlock: BlockConfig<JiraResponse> = {
{ label: 'Add Watcher', id: 'add_watcher' },
{ label: 'Remove Watcher', id: 'remove_watcher' },
{ label: 'Get Users', id: 'get_users' },
{ label: 'Search Users', id: 'search_users' },
],
value: () => 'read',
},
@@ -673,6 +674,31 @@ Return ONLY the comment text - no explanations.`,
placeholder: 'Maximum users to return (default: 50)',
condition: { field: 'operation', value: 'get_users' },
},
// Search Users fields
{
id: 'searchUsersQuery',
title: 'Search Query',
type: 'short-input',
required: true,
placeholder: 'Enter email address or display name to search',
condition: { field: 'operation', value: 'search_users' },
},
{
id: 'searchUsersMaxResults',
title: 'Max Results',
type: 'short-input',
placeholder: 'Maximum users to return (default: 50)',
condition: { field: 'operation', value: 'search_users' },
mode: 'advanced',
},
{
id: 'searchUsersStartAt',
title: 'Start At',
type: 'short-input',
placeholder: 'Pagination start index (default: 0)',
condition: { field: 'operation', value: 'search_users' },
mode: 'advanced',
},
// Trigger SubBlocks
...getTrigger('jira_issue_created').subBlocks,
...getTrigger('jira_issue_updated').subBlocks,
@@ -707,6 +733,7 @@ Return ONLY the comment text - no explanations.`,
'jira_add_watcher',
'jira_remove_watcher',
'jira_get_users',
'jira_search_users',
],
config: {
tool: (params) => {
@@ -767,6 +794,8 @@ Return ONLY the comment text - no explanations.`,
return 'jira_remove_watcher'
case 'get_users':
return 'jira_get_users'
case 'search_users':
return 'jira_search_users'
default:
return 'jira_retrieve'
}
@@ -1023,6 +1052,18 @@ Return ONLY the comment text - no explanations.`,
: undefined,
}
}
case 'search_users': {
return {
...baseParams,
query: params.searchUsersQuery,
maxResults: params.searchUsersMaxResults
? Number.parseInt(params.searchUsersMaxResults)
: undefined,
startAt: params.searchUsersStartAt
? Number.parseInt(params.searchUsersStartAt)
: undefined,
}
}
default:
return baseParams
}
@@ -1102,6 +1143,13 @@ Return ONLY the comment text - no explanations.`,
},
usersStartAt: { type: 'string', description: 'Pagination start index for users' },
usersMaxResults: { type: 'string', description: 'Maximum users to return' },
// Search Users operation inputs
searchUsersQuery: {
type: 'string',
description: 'Search query (email address or display name)',
},
searchUsersMaxResults: { type: 'string', description: 'Maximum users to return from search' },
searchUsersStartAt: { type: 'string', description: 'Pagination start index for user search' },
},
outputs: {
// Common outputs across all Jira operations

View File

@@ -39,10 +39,10 @@ type FolderResponse = { id: string; name: string }
type PlannerTask = { id: string; title: string }
const ensureCredential = (context: SelectorContext, key: SelectorKey): string => {
if (!context.credentialId) {
if (!context.oauthCredential) {
throw new Error(`Missing credential for selector ${key}`)
}
return context.credentialId
return context.oauthCredential
}
const ensureDomain = (context: SelectorContext, key: SelectorKey): string => {
@@ -66,9 +66,9 @@ const registry: Record<SelectorKey, SelectorDefinition> = {
getQueryKey: ({ context }: SelectorQueryArgs) => [
'selectors',
'airtable.bases',
context.credentialId ?? 'none',
context.oauthCredential ?? 'none',
],
enabled: ({ context }) => Boolean(context.credentialId),
enabled: ({ context }) => Boolean(context.oauthCredential),
fetchList: async ({ context }: SelectorQueryArgs) => {
const credentialId = ensureCredential(context, 'airtable.bases')
const body = JSON.stringify({ credential: credentialId, workflowId: context.workflowId })
@@ -104,10 +104,10 @@ const registry: Record<SelectorKey, SelectorDefinition> = {
getQueryKey: ({ context }: SelectorQueryArgs) => [
'selectors',
'airtable.tables',
context.credentialId ?? 'none',
context.oauthCredential ?? 'none',
context.baseId ?? 'none',
],
enabled: ({ context }) => Boolean(context.credentialId && context.baseId),
enabled: ({ context }) => Boolean(context.oauthCredential && context.baseId),
fetchList: async ({ context }: SelectorQueryArgs) => {
const credentialId = ensureCredential(context, 'airtable.tables')
if (!context.baseId) {
@@ -151,9 +151,9 @@ const registry: Record<SelectorKey, SelectorDefinition> = {
getQueryKey: ({ context }: SelectorQueryArgs) => [
'selectors',
'asana.workspaces',
context.credentialId ?? 'none',
context.oauthCredential ?? 'none',
],
enabled: ({ context }) => Boolean(context.credentialId),
enabled: ({ context }) => Boolean(context.oauthCredential),
fetchList: async ({ context }: SelectorQueryArgs) => {
const credentialId = ensureCredential(context, 'asana.workspaces')
const body = JSON.stringify({ credential: credentialId, workflowId: context.workflowId })
@@ -182,9 +182,9 @@ const registry: Record<SelectorKey, SelectorDefinition> = {
getQueryKey: ({ context }: SelectorQueryArgs) => [
'selectors',
'attio.objects',
context.credentialId ?? 'none',
context.oauthCredential ?? 'none',
],
enabled: ({ context }) => Boolean(context.credentialId),
enabled: ({ context }) => Boolean(context.oauthCredential),
fetchList: async ({ context }: SelectorQueryArgs) => {
const credentialId = ensureCredential(context, 'attio.objects')
const body = JSON.stringify({ credential: credentialId, workflowId: context.workflowId })
@@ -216,9 +216,9 @@ const registry: Record<SelectorKey, SelectorDefinition> = {
getQueryKey: ({ context }: SelectorQueryArgs) => [
'selectors',
'attio.lists',
context.credentialId ?? 'none',
context.oauthCredential ?? 'none',
],
enabled: ({ context }) => Boolean(context.credentialId),
enabled: ({ context }) => Boolean(context.oauthCredential),
fetchList: async ({ context }: SelectorQueryArgs) => {
const credentialId = ensureCredential(context, 'attio.lists')
const body = JSON.stringify({ credential: credentialId, workflowId: context.workflowId })
@@ -250,10 +250,10 @@ const registry: Record<SelectorKey, SelectorDefinition> = {
getQueryKey: ({ context }: SelectorQueryArgs) => [
'selectors',
'bigquery.datasets',
context.credentialId ?? 'none',
context.oauthCredential ?? 'none',
context.projectId ?? 'none',
],
enabled: ({ context }) => Boolean(context.credentialId && context.projectId),
enabled: ({ context }) => Boolean(context.oauthCredential && context.projectId),
fetchList: async ({ context }: SelectorQueryArgs) => {
const credentialId = ensureCredential(context, 'bigquery.datasets')
if (!context.projectId) throw new Error('Missing project ID for bigquery.datasets selector')
@@ -298,12 +298,12 @@ const registry: Record<SelectorKey, SelectorDefinition> = {
getQueryKey: ({ context }: SelectorQueryArgs) => [
'selectors',
'bigquery.tables',
context.credentialId ?? 'none',
context.oauthCredential ?? 'none',
context.projectId ?? 'none',
context.datasetId ?? 'none',
],
enabled: ({ context }) =>
Boolean(context.credentialId && context.projectId && context.datasetId),
Boolean(context.oauthCredential && context.projectId && context.datasetId),
fetchList: async ({ context }: SelectorQueryArgs) => {
const credentialId = ensureCredential(context, 'bigquery.tables')
if (!context.projectId) throw new Error('Missing project ID for bigquery.tables selector')
@@ -347,9 +347,9 @@ const registry: Record<SelectorKey, SelectorDefinition> = {
getQueryKey: ({ context }: SelectorQueryArgs) => [
'selectors',
'calcom.eventTypes',
context.credentialId ?? 'none',
context.oauthCredential ?? 'none',
],
enabled: ({ context }) => Boolean(context.credentialId),
enabled: ({ context }) => Boolean(context.oauthCredential),
fetchList: async ({ context }: SelectorQueryArgs) => {
const credentialId = ensureCredential(context, 'calcom.eventTypes')
const body = JSON.stringify({ credential: credentialId, workflowId: context.workflowId })
@@ -381,9 +381,9 @@ const registry: Record<SelectorKey, SelectorDefinition> = {
getQueryKey: ({ context }: SelectorQueryArgs) => [
'selectors',
'calcom.schedules',
context.credentialId ?? 'none',
context.oauthCredential ?? 'none',
],
enabled: ({ context }) => Boolean(context.credentialId),
enabled: ({ context }) => Boolean(context.oauthCredential),
fetchList: async ({ context }: SelectorQueryArgs) => {
const credentialId = ensureCredential(context, 'calcom.schedules')
const body = JSON.stringify({ credential: credentialId, workflowId: context.workflowId })
@@ -415,10 +415,10 @@ const registry: Record<SelectorKey, SelectorDefinition> = {
getQueryKey: ({ context }: SelectorQueryArgs) => [
'selectors',
'confluence.spaces',
context.credentialId ?? 'none',
context.oauthCredential ?? 'none',
context.domain ?? 'none',
],
enabled: ({ context }) => Boolean(context.credentialId && context.domain),
enabled: ({ context }) => Boolean(context.oauthCredential && context.domain),
fetchList: async ({ context }: SelectorQueryArgs) => {
const credentialId = ensureCredential(context, 'confluence.spaces')
const domain = ensureDomain(context, 'confluence.spaces')
@@ -460,10 +460,10 @@ const registry: Record<SelectorKey, SelectorDefinition> = {
getQueryKey: ({ context }: SelectorQueryArgs) => [
'selectors',
'jsm.serviceDesks',
context.credentialId ?? 'none',
context.oauthCredential ?? 'none',
context.domain ?? 'none',
],
enabled: ({ context }) => Boolean(context.credentialId && context.domain),
enabled: ({ context }) => Boolean(context.oauthCredential && context.domain),
fetchList: async ({ context }: SelectorQueryArgs) => {
const credentialId = ensureCredential(context, 'jsm.serviceDesks')
const domain = ensureDomain(context, 'jsm.serviceDesks')
@@ -505,12 +505,12 @@ const registry: Record<SelectorKey, SelectorDefinition> = {
getQueryKey: ({ context }: SelectorQueryArgs) => [
'selectors',
'jsm.requestTypes',
context.credentialId ?? 'none',
context.oauthCredential ?? 'none',
context.domain ?? 'none',
context.serviceDeskId ?? 'none',
],
enabled: ({ context }) =>
Boolean(context.credentialId && context.domain && context.serviceDeskId),
Boolean(context.oauthCredential && context.domain && context.serviceDeskId),
fetchList: async ({ context }: SelectorQueryArgs) => {
const credentialId = ensureCredential(context, 'jsm.requestTypes')
const domain = ensureDomain(context, 'jsm.requestTypes')
@@ -556,9 +556,9 @@ const registry: Record<SelectorKey, SelectorDefinition> = {
getQueryKey: ({ context }: SelectorQueryArgs) => [
'selectors',
'google.tasks.lists',
context.credentialId ?? 'none',
context.oauthCredential ?? 'none',
],
enabled: ({ context }) => Boolean(context.credentialId),
enabled: ({ context }) => Boolean(context.oauthCredential),
fetchList: async ({ context }: SelectorQueryArgs) => {
const credentialId = ensureCredential(context, 'google.tasks.lists')
const body = JSON.stringify({ credential: credentialId, workflowId: context.workflowId })
@@ -587,9 +587,9 @@ const registry: Record<SelectorKey, SelectorDefinition> = {
getQueryKey: ({ context }: SelectorQueryArgs) => [
'selectors',
'microsoft.planner.plans',
context.credentialId ?? 'none',
context.oauthCredential ?? 'none',
],
enabled: ({ context }) => Boolean(context.credentialId),
enabled: ({ context }) => Boolean(context.oauthCredential),
fetchList: async ({ context }: SelectorQueryArgs) => {
const credentialId = ensureCredential(context, 'microsoft.planner.plans')
const body = JSON.stringify({ credential: credentialId, workflowId: context.workflowId })
@@ -618,9 +618,9 @@ const registry: Record<SelectorKey, SelectorDefinition> = {
getQueryKey: ({ context }: SelectorQueryArgs) => [
'selectors',
'notion.databases',
context.credentialId ?? 'none',
context.oauthCredential ?? 'none',
],
enabled: ({ context }) => Boolean(context.credentialId),
enabled: ({ context }) => Boolean(context.oauthCredential),
fetchList: async ({ context }: SelectorQueryArgs) => {
const credentialId = ensureCredential(context, 'notion.databases')
const body = JSON.stringify({ credential: credentialId, workflowId: context.workflowId })
@@ -652,9 +652,9 @@ const registry: Record<SelectorKey, SelectorDefinition> = {
getQueryKey: ({ context }: SelectorQueryArgs) => [
'selectors',
'notion.pages',
context.credentialId ?? 'none',
context.oauthCredential ?? 'none',
],
enabled: ({ context }) => Boolean(context.credentialId),
enabled: ({ context }) => Boolean(context.oauthCredential),
fetchList: async ({ context }: SelectorQueryArgs) => {
const credentialId = ensureCredential(context, 'notion.pages')
const body = JSON.stringify({ credential: credentialId, workflowId: context.workflowId })
@@ -686,9 +686,9 @@ const registry: Record<SelectorKey, SelectorDefinition> = {
getQueryKey: ({ context }: SelectorQueryArgs) => [
'selectors',
'pipedrive.pipelines',
context.credentialId ?? 'none',
context.oauthCredential ?? 'none',
],
enabled: ({ context }) => Boolean(context.credentialId),
enabled: ({ context }) => Boolean(context.oauthCredential),
fetchList: async ({ context }: SelectorQueryArgs) => {
const credentialId = ensureCredential(context, 'pipedrive.pipelines')
const body = JSON.stringify({ credential: credentialId, workflowId: context.workflowId })
@@ -720,10 +720,10 @@ const registry: Record<SelectorKey, SelectorDefinition> = {
getQueryKey: ({ context }: SelectorQueryArgs) => [
'selectors',
'sharepoint.lists',
context.credentialId ?? 'none',
context.oauthCredential ?? 'none',
context.siteId ?? 'none',
],
enabled: ({ context }) => Boolean(context.credentialId && context.siteId),
enabled: ({ context }) => Boolean(context.oauthCredential && context.siteId),
fetchList: async ({ context }: SelectorQueryArgs) => {
const credentialId = ensureCredential(context, 'sharepoint.lists')
if (!context.siteId) throw new Error('Missing site ID for sharepoint.lists selector')
@@ -761,9 +761,9 @@ const registry: Record<SelectorKey, SelectorDefinition> = {
getQueryKey: ({ context }: SelectorQueryArgs) => [
'selectors',
'trello.boards',
context.credentialId ?? 'none',
context.oauthCredential ?? 'none',
],
enabled: ({ context }) => Boolean(context.credentialId),
enabled: ({ context }) => Boolean(context.oauthCredential),
fetchList: async ({ context }: SelectorQueryArgs) => {
const credentialId = ensureCredential(context, 'trello.boards')
const body = JSON.stringify({ credential: credentialId, workflowId: context.workflowId })
@@ -794,9 +794,9 @@ const registry: Record<SelectorKey, SelectorDefinition> = {
getQueryKey: ({ context }: SelectorQueryArgs) => [
'selectors',
'zoom.meetings',
context.credentialId ?? 'none',
context.oauthCredential ?? 'none',
],
enabled: ({ context }) => Boolean(context.credentialId),
enabled: ({ context }) => Boolean(context.oauthCredential),
fetchList: async ({ context }: SelectorQueryArgs) => {
const credentialId = ensureCredential(context, 'zoom.meetings')
const body = JSON.stringify({ credential: credentialId, workflowId: context.workflowId })
@@ -828,12 +828,12 @@ const registry: Record<SelectorKey, SelectorDefinition> = {
getQueryKey: ({ context }: SelectorQueryArgs) => [
'selectors',
'slack.channels',
context.credentialId ?? 'none',
context.oauthCredential ?? 'none',
],
enabled: ({ context }) => Boolean(context.credentialId),
enabled: ({ context }) => Boolean(context.oauthCredential),
fetchList: async ({ context }: SelectorQueryArgs) => {
const body = JSON.stringify({
credential: context.credentialId,
credential: context.oauthCredential,
workflowId: context.workflowId,
})
const data = await fetchJson<{ channels: SlackChannel[] }>('/api/tools/slack/channels', {
@@ -852,12 +852,12 @@ const registry: Record<SelectorKey, SelectorDefinition> = {
getQueryKey: ({ context }: SelectorQueryArgs) => [
'selectors',
'slack.users',
context.credentialId ?? 'none',
context.oauthCredential ?? 'none',
],
enabled: ({ context }) => Boolean(context.credentialId),
enabled: ({ context }) => Boolean(context.oauthCredential),
fetchList: async ({ context }: SelectorQueryArgs) => {
const body = JSON.stringify({
credential: context.credentialId,
credential: context.oauthCredential,
workflowId: context.workflowId,
})
const data = await fetchJson<{ users: SlackUser[] }>('/api/tools/slack/users', {
@@ -876,12 +876,12 @@ const registry: Record<SelectorKey, SelectorDefinition> = {
getQueryKey: ({ context }: SelectorQueryArgs) => [
'selectors',
'gmail.labels',
context.credentialId ?? 'none',
context.oauthCredential ?? 'none',
],
enabled: ({ context }) => Boolean(context.credentialId),
enabled: ({ context }) => Boolean(context.oauthCredential),
fetchList: async ({ context }: SelectorQueryArgs) => {
const data = await fetchJson<{ labels: FolderResponse[] }>('/api/tools/gmail/labels', {
searchParams: { credentialId: context.credentialId },
searchParams: { credentialId: context.oauthCredential },
})
return (data.labels || []).map((label) => ({
id: label.id,
@@ -895,12 +895,12 @@ const registry: Record<SelectorKey, SelectorDefinition> = {
getQueryKey: ({ context }: SelectorQueryArgs) => [
'selectors',
'outlook.folders',
context.credentialId ?? 'none',
context.oauthCredential ?? 'none',
],
enabled: ({ context }) => Boolean(context.credentialId),
enabled: ({ context }) => Boolean(context.oauthCredential),
fetchList: async ({ context }: SelectorQueryArgs) => {
const data = await fetchJson<{ folders: FolderResponse[] }>('/api/tools/outlook/folders', {
searchParams: { credentialId: context.credentialId },
searchParams: { credentialId: context.oauthCredential },
})
return (data.folders || []).map((folder) => ({
id: folder.id,
@@ -914,13 +914,13 @@ const registry: Record<SelectorKey, SelectorDefinition> = {
getQueryKey: ({ context }: SelectorQueryArgs) => [
'selectors',
'google.calendar',
context.credentialId ?? 'none',
context.oauthCredential ?? 'none',
],
enabled: ({ context }) => Boolean(context.credentialId),
enabled: ({ context }) => Boolean(context.oauthCredential),
fetchList: async ({ context }: SelectorQueryArgs) => {
const data = await fetchJson<{ calendars: { id: string; summary: string }[] }>(
'/api/tools/google_calendar/calendars',
{ searchParams: { credentialId: context.credentialId } }
{ searchParams: { credentialId: context.oauthCredential } }
)
return (data.calendars || []).map((calendar) => ({
id: calendar.id,
@@ -934,11 +934,11 @@ const registry: Record<SelectorKey, SelectorDefinition> = {
getQueryKey: ({ context }: SelectorQueryArgs) => [
'selectors',
'microsoft.teams',
context.credentialId ?? 'none',
context.oauthCredential ?? 'none',
],
enabled: ({ context }) => Boolean(context.credentialId),
enabled: ({ context }) => Boolean(context.oauthCredential),
fetchList: async ({ context }: SelectorQueryArgs) => {
const body = JSON.stringify({ credential: context.credentialId })
const body = JSON.stringify({ credential: context.oauthCredential })
const data = await fetchJson<{ teams: { id: string; displayName: string }[] }>(
'/api/tools/microsoft-teams/teams',
{ method: 'POST', body }
@@ -955,11 +955,11 @@ const registry: Record<SelectorKey, SelectorDefinition> = {
getQueryKey: ({ context }: SelectorQueryArgs) => [
'selectors',
'microsoft.chats',
context.credentialId ?? 'none',
context.oauthCredential ?? 'none',
],
enabled: ({ context }) => Boolean(context.credentialId),
enabled: ({ context }) => Boolean(context.oauthCredential),
fetchList: async ({ context }: SelectorQueryArgs) => {
const body = JSON.stringify({ credential: context.credentialId })
const body = JSON.stringify({ credential: context.oauthCredential })
const data = await fetchJson<{ chats: { id: string; displayName: string }[] }>(
'/api/tools/microsoft-teams/chats',
{ method: 'POST', body }
@@ -976,13 +976,13 @@ const registry: Record<SelectorKey, SelectorDefinition> = {
getQueryKey: ({ context }: SelectorQueryArgs) => [
'selectors',
'microsoft.channels',
context.credentialId ?? 'none',
context.oauthCredential ?? 'none',
context.teamId ?? 'none',
],
enabled: ({ context }) => Boolean(context.credentialId && context.teamId),
enabled: ({ context }) => Boolean(context.oauthCredential && context.teamId),
fetchList: async ({ context }: SelectorQueryArgs) => {
const body = JSON.stringify({
credential: context.credentialId,
credential: context.oauthCredential,
teamId: context.teamId,
})
const data = await fetchJson<{ channels: { id: string; displayName: string }[] }>(
@@ -1001,14 +1001,14 @@ const registry: Record<SelectorKey, SelectorDefinition> = {
getQueryKey: ({ context }: SelectorQueryArgs) => [
'selectors',
'wealthbox.contacts',
context.credentialId ?? 'none',
context.oauthCredential ?? 'none',
],
enabled: ({ context }) => Boolean(context.credentialId),
enabled: ({ context }) => Boolean(context.oauthCredential),
fetchList: async ({ context }: SelectorQueryArgs) => {
const data = await fetchJson<{ items: { id: string; name: string }[] }>(
'/api/tools/wealthbox/items',
{
searchParams: { credentialId: context.credentialId, type: 'contact' },
searchParams: { credentialId: context.oauthCredential, type: 'contact' },
}
)
return (data.items || []).map((item) => ({
@@ -1023,9 +1023,9 @@ const registry: Record<SelectorKey, SelectorDefinition> = {
getQueryKey: ({ context }: SelectorQueryArgs) => [
'selectors',
'sharepoint.sites',
context.credentialId ?? 'none',
context.oauthCredential ?? 'none',
],
enabled: ({ context }) => Boolean(context.credentialId),
enabled: ({ context }) => Boolean(context.oauthCredential),
fetchList: async ({ context }: SelectorQueryArgs) => {
const credentialId = ensureCredential(context, 'sharepoint.sites')
const body = JSON.stringify({
@@ -1069,10 +1069,10 @@ const registry: Record<SelectorKey, SelectorDefinition> = {
getQueryKey: ({ context }: SelectorQueryArgs) => [
'selectors',
'microsoft.planner',
context.credentialId ?? 'none',
context.oauthCredential ?? 'none',
context.planId ?? 'none',
],
enabled: ({ context }) => Boolean(context.credentialId && context.planId),
enabled: ({ context }) => Boolean(context.oauthCredential && context.planId),
fetchList: async ({ context }: SelectorQueryArgs) => {
const credentialId = ensureCredential(context, 'microsoft.planner')
const body = JSON.stringify({
@@ -1112,11 +1112,11 @@ const registry: Record<SelectorKey, SelectorDefinition> = {
getQueryKey: ({ context, search }: SelectorQueryArgs) => [
'selectors',
'jira.projects',
context.credentialId ?? 'none',
context.oauthCredential ?? 'none',
context.domain ?? 'none',
search ?? '',
],
enabled: ({ context }) => Boolean(context.credentialId && context.domain),
enabled: ({ context }) => Boolean(context.oauthCredential && context.domain),
fetchList: async ({ context, search }: SelectorQueryArgs) => {
const credentialId = ensureCredential(context, 'jira.projects')
const domain = ensureDomain(context, 'jira.projects')
@@ -1171,12 +1171,12 @@ const registry: Record<SelectorKey, SelectorDefinition> = {
getQueryKey: ({ context, search }: SelectorQueryArgs) => [
'selectors',
'jira.issues',
context.credentialId ?? 'none',
context.oauthCredential ?? 'none',
context.domain ?? 'none',
context.projectId ?? 'none',
search ?? '',
],
enabled: ({ context }) => Boolean(context.credentialId && context.domain),
enabled: ({ context }) => Boolean(context.oauthCredential && context.domain),
fetchList: async ({ context, search }: SelectorQueryArgs) => {
const credentialId = ensureCredential(context, 'jira.issues')
const domain = ensureDomain(context, 'jira.issues')
@@ -1235,9 +1235,9 @@ const registry: Record<SelectorKey, SelectorDefinition> = {
getQueryKey: ({ context }: SelectorQueryArgs) => [
'selectors',
'linear.teams',
context.credentialId ?? 'none',
context.oauthCredential ?? 'none',
],
enabled: ({ context }) => Boolean(context.credentialId),
enabled: ({ context }) => Boolean(context.oauthCredential),
fetchList: async ({ context }: SelectorQueryArgs) => {
const credentialId = ensureCredential(context, 'linear.teams')
const body = JSON.stringify({ credential: credentialId, workflowId: context.workflowId })
@@ -1260,10 +1260,10 @@ const registry: Record<SelectorKey, SelectorDefinition> = {
getQueryKey: ({ context }: SelectorQueryArgs) => [
'selectors',
'linear.projects',
context.credentialId ?? 'none',
context.oauthCredential ?? 'none',
context.teamId ?? 'none',
],
enabled: ({ context }) => Boolean(context.credentialId && context.teamId),
enabled: ({ context }) => Boolean(context.oauthCredential && context.teamId),
fetchList: async ({ context }: SelectorQueryArgs) => {
const credentialId = ensureCredential(context, 'linear.projects')
const body = JSON.stringify({
@@ -1290,11 +1290,11 @@ const registry: Record<SelectorKey, SelectorDefinition> = {
getQueryKey: ({ context, search }: SelectorQueryArgs) => [
'selectors',
'confluence.pages',
context.credentialId ?? 'none',
context.oauthCredential ?? 'none',
context.domain ?? 'none',
search ?? '',
],
enabled: ({ context }) => Boolean(context.credentialId && context.domain),
enabled: ({ context }) => Boolean(context.oauthCredential && context.domain),
fetchList: async ({ context, search }: SelectorQueryArgs) => {
const credentialId = ensureCredential(context, 'confluence.pages')
const domain = ensureDomain(context, 'confluence.pages')
@@ -1343,9 +1343,9 @@ const registry: Record<SelectorKey, SelectorDefinition> = {
getQueryKey: ({ context }: SelectorQueryArgs) => [
'selectors',
'onedrive.files',
context.credentialId ?? 'none',
context.oauthCredential ?? 'none',
],
enabled: ({ context }) => Boolean(context.credentialId),
enabled: ({ context }) => Boolean(context.oauthCredential),
fetchList: async ({ context }: SelectorQueryArgs) => {
const credentialId = ensureCredential(context, 'onedrive.files')
const data = await fetchJson<{ files: { id: string; name: string }[] }>(
@@ -1366,9 +1366,9 @@ const registry: Record<SelectorKey, SelectorDefinition> = {
getQueryKey: ({ context }: SelectorQueryArgs) => [
'selectors',
'onedrive.folders',
context.credentialId ?? 'none',
context.oauthCredential ?? 'none',
],
enabled: ({ context }) => Boolean(context.credentialId),
enabled: ({ context }) => Boolean(context.oauthCredential),
fetchList: async ({ context }: SelectorQueryArgs) => {
const credentialId = ensureCredential(context, 'onedrive.folders')
const data = await fetchJson<{ files: { id: string; name: string }[] }>(
@@ -1389,12 +1389,12 @@ const registry: Record<SelectorKey, SelectorDefinition> = {
getQueryKey: ({ context, search }: SelectorQueryArgs) => [
'selectors',
'google.drive',
context.credentialId ?? 'none',
context.oauthCredential ?? 'none',
context.mimeType ?? 'any',
context.fileId ?? 'root',
search ?? '',
],
enabled: ({ context }) => Boolean(context.credentialId),
enabled: ({ context }) => Boolean(context.oauthCredential),
fetchList: async ({ context, search }: SelectorQueryArgs) => {
const credentialId = ensureCredential(context, 'google.drive')
const data = await fetchJson<{ files: { id: string; name: string }[] }>(
@@ -1438,10 +1438,10 @@ const registry: Record<SelectorKey, SelectorDefinition> = {
getQueryKey: ({ context }: SelectorQueryArgs) => [
'selectors',
'google.sheets',
context.credentialId ?? 'none',
context.oauthCredential ?? 'none',
context.spreadsheetId ?? 'none',
],
enabled: ({ context }) => Boolean(context.credentialId && context.spreadsheetId),
enabled: ({ context }) => Boolean(context.oauthCredential && context.spreadsheetId),
fetchList: async ({ context }: SelectorQueryArgs) => {
const credentialId = ensureCredential(context, 'google.sheets')
if (!context.spreadsheetId) {
@@ -1469,10 +1469,10 @@ const registry: Record<SelectorKey, SelectorDefinition> = {
getQueryKey: ({ context }: SelectorQueryArgs) => [
'selectors',
'microsoft.excel.sheets',
context.credentialId ?? 'none',
context.oauthCredential ?? 'none',
context.spreadsheetId ?? 'none',
],
enabled: ({ context }) => Boolean(context.credentialId && context.spreadsheetId),
enabled: ({ context }) => Boolean(context.oauthCredential && context.spreadsheetId),
fetchList: async ({ context }: SelectorQueryArgs) => {
const credentialId = ensureCredential(context, 'microsoft.excel.sheets')
if (!context.spreadsheetId) {
@@ -1500,10 +1500,10 @@ const registry: Record<SelectorKey, SelectorDefinition> = {
getQueryKey: ({ context, search }: SelectorQueryArgs) => [
'selectors',
'microsoft.excel',
context.credentialId ?? 'none',
context.oauthCredential ?? 'none',
search ?? '',
],
enabled: ({ context }) => Boolean(context.credentialId),
enabled: ({ context }) => Boolean(context.oauthCredential),
fetchList: async ({ context, search }: SelectorQueryArgs) => {
const credentialId = ensureCredential(context, 'microsoft.excel')
const data = await fetchJson<{ files: { id: string; name: string }[] }>(
@@ -1528,10 +1528,10 @@ const registry: Record<SelectorKey, SelectorDefinition> = {
getQueryKey: ({ context, search }: SelectorQueryArgs) => [
'selectors',
'microsoft.word',
context.credentialId ?? 'none',
context.oauthCredential ?? 'none',
search ?? '',
],
enabled: ({ context }) => Boolean(context.credentialId),
enabled: ({ context }) => Boolean(context.oauthCredential),
fetchList: async ({ context, search }: SelectorQueryArgs) => {
const credentialId = ensureCredential(context, 'microsoft.word')
const data = await fetchJson<{ files: { id: string; name: string }[] }>(
@@ -1596,9 +1596,9 @@ const registry: Record<SelectorKey, SelectorDefinition> = {
getQueryKey: ({ context }: SelectorQueryArgs) => [
'selectors',
'webflow.sites',
context.credentialId ?? 'none',
context.oauthCredential ?? 'none',
],
enabled: ({ context }) => Boolean(context.credentialId),
enabled: ({ context }) => Boolean(context.oauthCredential),
fetchList: async ({ context }: SelectorQueryArgs) => {
const credentialId = ensureCredential(context, 'webflow.sites')
const body = JSON.stringify({ credential: credentialId, workflowId: context.workflowId })
@@ -1621,10 +1621,10 @@ const registry: Record<SelectorKey, SelectorDefinition> = {
getQueryKey: ({ context }: SelectorQueryArgs) => [
'selectors',
'webflow.collections',
context.credentialId ?? 'none',
context.oauthCredential ?? 'none',
context.siteId ?? 'none',
],
enabled: ({ context }) => Boolean(context.credentialId && context.siteId),
enabled: ({ context }) => Boolean(context.oauthCredential && context.siteId),
fetchList: async ({ context }: SelectorQueryArgs) => {
const credentialId = ensureCredential(context, 'webflow.collections')
if (!context.siteId) {
@@ -1654,11 +1654,11 @@ const registry: Record<SelectorKey, SelectorDefinition> = {
getQueryKey: ({ context, search }: SelectorQueryArgs) => [
'selectors',
'webflow.items',
context.credentialId ?? 'none',
context.oauthCredential ?? 'none',
context.collectionId ?? 'none',
search ?? '',
],
enabled: ({ context }) => Boolean(context.credentialId && context.collectionId),
enabled: ({ context }) => Boolean(context.oauthCredential && context.collectionId),
fetchList: async ({ context, search }: SelectorQueryArgs) => {
const credentialId = ensureCredential(context, 'webflow.items')
if (!context.collectionId) {

View File

@@ -7,46 +7,16 @@ export interface SelectorResolution {
allowSearch: boolean
}
export interface SelectorResolutionArgs {
workflowId?: string
credentialId?: string
domain?: string
projectId?: string
planId?: string
teamId?: string
knowledgeBaseId?: string
siteId?: string
collectionId?: string
spreadsheetId?: string
fileId?: string
baseId?: string
datasetId?: string
serviceDeskId?: string
}
export function resolveSelectorForSubBlock(
subBlock: SubBlockConfig,
args: SelectorResolutionArgs
context: SelectorContext
): SelectorResolution | null {
if (!subBlock.selectorKey) return null
return {
key: subBlock.selectorKey,
context: {
workflowId: args.workflowId,
credentialId: args.credentialId,
domain: args.domain,
projectId: args.projectId,
planId: args.planId,
teamId: args.teamId,
knowledgeBaseId: args.knowledgeBaseId,
siteId: args.siteId,
collectionId: args.collectionId,
spreadsheetId: args.spreadsheetId,
fileId: args.fileId,
baseId: args.baseId,
datasetId: args.datasetId,
serviceDeskId: args.serviceDeskId,
mimeType: subBlock.mimeType,
...context,
mimeType: subBlock.mimeType ?? context.mimeType,
},
allowSearch: subBlock.selectorAllowSearch ?? true,
}

View File

@@ -61,7 +61,7 @@ export interface SelectorOption {
export interface SelectorContext {
workspaceId?: string
workflowId?: string
credentialId?: string
oauthCredential?: string
serviceId?: string
domain?: string
teamId?: string

View File

@@ -12,7 +12,7 @@ interface SelectorDisplayNameArgs {
subBlock?: SubBlockConfig
value: unknown
workflowId?: string
credentialId?: string
oauthCredential?: string
domain?: string
projectId?: string
planId?: string
@@ -31,7 +31,7 @@ export function useSelectorDisplayName({
subBlock,
value,
workflowId,
credentialId,
oauthCredential,
domain,
projectId,
planId,
@@ -51,7 +51,7 @@ export function useSelectorDisplayName({
if (!subBlock || !detailId) return null
return resolveSelectorForSubBlock(subBlock, {
workflowId,
credentialId,
oauthCredential,
domain,
projectId,
planId,
@@ -69,7 +69,7 @@ export function useSelectorDisplayName({
subBlock,
detailId,
workflowId,
credentialId,
oauthCredential,
domain,
projectId,
planId,

View File

@@ -1,4 +1,5 @@
import { createLogger } from '@sim/logger'
import { buildSelectorContextFromBlock } from '@/lib/workflows/subblocks/context'
import { getBlock } from '@/blocks/registry'
import { SELECTOR_TYPES_HYDRATION_REQUIRED, type SubBlockConfig } from '@/blocks/types'
import { CREDENTIAL_SET, isUuid } from '@/executor/constants'
@@ -6,7 +7,7 @@ import { fetchCredentialSetById } from '@/hooks/queries/credential-sets'
import { fetchOAuthCredentialDetail } from '@/hooks/queries/oauth-credentials'
import { getSelectorDefinition } from '@/hooks/selectors/registry'
import { resolveSelectorForSubBlock } from '@/hooks/selectors/resolution'
import type { SelectorKey } from '@/hooks/selectors/types'
import type { SelectorContext, SelectorKey } from '@/hooks/selectors/types'
import type { WorkflowState } from '@/stores/workflows/workflow/types'
const logger = createLogger('ResolveValues')
@@ -39,74 +40,8 @@ interface ResolutionContext {
blockId?: string
}
/**
* Extended context extracted from block subBlocks for selector resolution
*/
interface ExtendedSelectorContext {
credentialId?: string
domain?: string
projectId?: string
planId?: string
teamId?: string
knowledgeBaseId?: string
siteId?: string
collectionId?: string
spreadsheetId?: string
baseId?: string
datasetId?: string
serviceDeskId?: string
}
function getSemanticFallback(subBlockId: string, subBlockConfig?: SubBlockConfig): string {
if (subBlockConfig?.title) {
return subBlockConfig.title.toLowerCase()
}
const patterns: Record<string, string> = {
credential: 'credential',
channel: 'channel',
channelId: 'channel',
user: 'user',
userId: 'user',
workflow: 'workflow',
workflowId: 'workflow',
file: 'file',
fileId: 'file',
folder: 'folder',
folderId: 'folder',
project: 'project',
projectId: 'project',
team: 'team',
teamId: 'team',
sheet: 'sheet',
sheetId: 'sheet',
document: 'document',
documentId: 'document',
knowledgeBase: 'knowledge base',
knowledgeBaseId: 'knowledge base',
server: 'server',
serverId: 'server',
tool: 'tool',
toolId: 'tool',
calendar: 'calendar',
calendarId: 'calendar',
label: 'label',
labelId: 'label',
site: 'site',
siteId: 'site',
collection: 'collection',
collectionId: 'collection',
item: 'item',
itemId: 'item',
contact: 'contact',
contactId: 'contact',
task: 'task',
taskId: 'task',
chat: 'chat',
chatId: 'chat',
}
return patterns[subBlockId] || 'value'
function getSemanticFallback(subBlockConfig: SubBlockConfig): string {
return (subBlockConfig.title ?? subBlockConfig.id).toLowerCase()
}
async function resolveCredential(credentialId: string, workflowId: string): Promise<string | null> {
@@ -150,26 +85,10 @@ async function resolveWorkflow(workflowId: string): Promise<string | null> {
async function resolveSelectorValue(
value: string,
selectorKey: SelectorKey,
extendedContext: ExtendedSelectorContext,
workflowId: string
selectorContext: SelectorContext
): Promise<string | null> {
try {
const definition = getSelectorDefinition(selectorKey)
const selectorContext = {
workflowId,
credentialId: extendedContext.credentialId,
domain: extendedContext.domain,
projectId: extendedContext.projectId,
planId: extendedContext.planId,
teamId: extendedContext.teamId,
knowledgeBaseId: extendedContext.knowledgeBaseId,
siteId: extendedContext.siteId,
collectionId: extendedContext.collectionId,
spreadsheetId: extendedContext.spreadsheetId,
baseId: extendedContext.baseId,
datasetId: extendedContext.datasetId,
serviceDeskId: extendedContext.serviceDeskId,
}
if (definition.fetchById) {
const result = await definition.fetchById({
@@ -219,37 +138,14 @@ export function formatValueForDisplay(value: unknown): string {
return String(value)
}
/**
* Extracts extended context from a block's subBlocks for selector resolution.
* This mirrors the context extraction done in the UI components.
*/
function extractExtendedContext(
function extractSelectorContext(
blockId: string,
currentState: WorkflowState
): ExtendedSelectorContext {
currentState: WorkflowState,
workflowId: string
): SelectorContext {
const block = currentState.blocks?.[blockId]
if (!block?.subBlocks) return {}
const getStringValue = (id: string): string | undefined => {
const subBlock = block.subBlocks[id] as { value?: unknown } | undefined
const val = subBlock?.value
return typeof val === 'string' ? val : undefined
}
return {
credentialId: getStringValue('credential'),
domain: getStringValue('domain'),
projectId: getStringValue('projectId'),
planId: getStringValue('planId'),
teamId: getStringValue('teamId'),
knowledgeBaseId: getStringValue('knowledgeBaseId'),
siteId: getStringValue('siteId'),
collectionId: getStringValue('collectionId'),
spreadsheetId: getStringValue('spreadsheetId') || getStringValue('fileId'),
baseId: getStringValue('baseId') || getStringValue('baseSelector'),
datasetId: getStringValue('datasetId') || getStringValue('datasetSelector'),
serviceDeskId: getStringValue('serviceDeskId') || getStringValue('serviceDeskSelector'),
}
if (!block?.subBlocks) return { workflowId }
return buildSelectorContextFromBlock(block.type, block.subBlocks, { workflowId })
}
/**
@@ -275,11 +171,14 @@ export async function resolveValueForDisplay(
const blockConfig = getBlock(context.blockType)
const subBlockConfig = blockConfig?.subBlocks.find((sb) => sb.id === context.subBlockId)
const semanticFallback = getSemanticFallback(context.subBlockId, subBlockConfig)
if (!subBlockConfig) {
return { original: value, displayLabel: formatValueForDisplay(value), resolved: false }
}
const semanticFallback = getSemanticFallback(subBlockConfig)
const extendedContext = context.blockId
? extractExtendedContext(context.blockId, context.currentState)
: {}
const selectorCtx = context.blockId
? extractSelectorContext(context.blockId, context.currentState, context.workflowId)
: { workflowId: context.workflowId }
// Credential fields (oauth-input or credential subBlockId)
const isCredentialField =
@@ -311,29 +210,10 @@ export async function resolveValueForDisplay(
// Selector types that require hydration (file-selector, sheet-selector, etc.)
// These support external service IDs like Google Drive file IDs
if (subBlockConfig && SELECTOR_TYPES_HYDRATION_REQUIRED.includes(subBlockConfig.type)) {
const resolution = resolveSelectorForSubBlock(subBlockConfig, {
workflowId: context.workflowId,
credentialId: extendedContext.credentialId,
domain: extendedContext.domain,
projectId: extendedContext.projectId,
planId: extendedContext.planId,
teamId: extendedContext.teamId,
knowledgeBaseId: extendedContext.knowledgeBaseId,
siteId: extendedContext.siteId,
collectionId: extendedContext.collectionId,
spreadsheetId: extendedContext.spreadsheetId,
baseId: extendedContext.baseId,
datasetId: extendedContext.datasetId,
serviceDeskId: extendedContext.serviceDeskId,
})
const resolution = resolveSelectorForSubBlock(subBlockConfig, selectorCtx)
if (resolution?.key) {
const label = await resolveSelectorValue(
value,
resolution.key,
extendedContext,
context.workflowId
)
const label = await resolveSelectorValue(value, resolution.key, selectorCtx)
if (label) {
return { original: value, displayLabel: label, resolved: true }
}

View File

@@ -117,6 +117,10 @@ export async function loadDeployedWorkflowState(
resolvedWorkspaceId = wfRow?.workspaceId ?? undefined
}
if (!resolvedWorkspaceId) {
throw new Error(`Workflow ${workflowId} has no workspace`)
}
const { blocks: migratedBlocks } = await applyBlockMigrations(
state.blocks || {},
resolvedWorkspaceId
@@ -139,7 +143,7 @@ export async function loadDeployedWorkflowState(
interface MigrationContext {
blocks: Record<string, BlockState>
workspaceId?: string
workspaceId: string
migrated: boolean
}
@@ -148,7 +152,7 @@ type BlockMigration = (ctx: MigrationContext) => MigrationContext | Promise<Migr
function createMigrationPipeline(migrations: BlockMigration[]) {
return async (
blocks: Record<string, BlockState>,
workspaceId?: string
workspaceId: string
): Promise<{ blocks: Record<string, BlockState>; migrated: boolean }> => {
let ctx: MigrationContext = { blocks, workspaceId, migrated: false }
for (const migration of migrations) {
@@ -170,7 +174,6 @@ const applyBlockMigrations = createMigrationPipeline([
}),
async (ctx) => {
if (!ctx.workspaceId) return ctx
const { blocks, migrated } = await migrateCredentialIds(ctx.blocks, ctx.workspaceId)
return { ...ctx, blocks, migrated: ctx.migrated || migrated }
},
@@ -409,9 +412,13 @@ export async function loadWorkflowFromNormalizedTables(
blocksMap[block.id] = assembled
})
if (!workflowRow?.workspaceId) {
throw new Error(`Workflow ${workflowId} has no workspace`)
}
const { blocks: finalBlocks, migrated } = await applyBlockMigrations(
blocksMap,
workflowRow?.workspaceId ?? undefined
workflowRow.workspaceId
)
if (migrated) {

View File

@@ -0,0 +1,125 @@
/**
* @vitest-environment node
*/
import { describe, expect, it, vi } from 'vitest'
vi.unmock('@/blocks/registry')
import { getAllBlocks } from '@/blocks/registry'
import { buildSelectorContextFromBlock, SELECTOR_CONTEXT_FIELDS } from './context'
import { buildCanonicalIndex, isCanonicalPair } from './visibility'
describe('buildSelectorContextFromBlock', () => {
it('should extract knowledgeBaseId from knowledgeBaseSelector via canonical mapping', () => {
const ctx = buildSelectorContextFromBlock('knowledge', {
operation: { id: 'operation', type: 'dropdown', value: 'search' },
knowledgeBaseSelector: {
id: 'knowledgeBaseSelector',
type: 'knowledge-base-selector',
value: 'kb-uuid-123',
},
})
expect(ctx.knowledgeBaseId).toBe('kb-uuid-123')
})
it('should extract knowledgeBaseId from manualKnowledgeBaseId via canonical mapping', () => {
const ctx = buildSelectorContextFromBlock('knowledge', {
operation: { id: 'operation', type: 'dropdown', value: 'search' },
manualKnowledgeBaseId: {
id: 'manualKnowledgeBaseId',
type: 'short-input',
value: 'manual-kb-id',
},
})
expect(ctx.knowledgeBaseId).toBe('manual-kb-id')
})
it('should skip null/empty values', () => {
const ctx = buildSelectorContextFromBlock('knowledge', {
knowledgeBaseSelector: {
id: 'knowledgeBaseSelector',
type: 'knowledge-base-selector',
value: '',
},
})
expect(ctx.knowledgeBaseId).toBeUndefined()
})
it('should return empty context for unknown block types', () => {
const ctx = buildSelectorContextFromBlock('nonexistent_block', {
foo: { id: 'foo', type: 'short-input', value: 'bar' },
})
expect(ctx).toEqual({})
})
it('should pass through workflowId from opts', () => {
const ctx = buildSelectorContextFromBlock(
'knowledge',
{ operation: { id: 'operation', type: 'dropdown', value: 'search' } },
{ workflowId: 'wf-123' }
)
expect(ctx.workflowId).toBe('wf-123')
})
it('should ignore subblock keys not in SELECTOR_CONTEXT_FIELDS', () => {
const ctx = buildSelectorContextFromBlock('knowledge', {
operation: { id: 'operation', type: 'dropdown', value: 'search' },
query: { id: 'query', type: 'short-input', value: 'some search query' },
})
expect((ctx as Record<string, unknown>).query).toBeUndefined()
expect((ctx as Record<string, unknown>).operation).toBeUndefined()
})
})
describe('SELECTOR_CONTEXT_FIELDS validation', () => {
it('every entry must be a canonicalParamId (if a canonical pair exists) or a direct subblock ID', () => {
const allCanonicalParamIds = new Set<string>()
const allSubBlockIds = new Set<string>()
const idsInCanonicalPairs = new Set<string>()
for (const block of getAllBlocks()) {
const index = buildCanonicalIndex(block.subBlocks)
for (const sb of block.subBlocks) {
allSubBlockIds.add(sb.id)
if (sb.canonicalParamId) {
allCanonicalParamIds.add(sb.canonicalParamId)
}
}
for (const group of Object.values(index.groupsById)) {
if (!isCanonicalPair(group)) continue
if (group.basicId) idsInCanonicalPairs.add(group.basicId)
for (const advId of group.advancedIds) idsInCanonicalPairs.add(advId)
}
}
const errors: string[] = []
for (const field of SELECTOR_CONTEXT_FIELDS) {
const f = field as string
if (allCanonicalParamIds.has(f)) continue
if (idsInCanonicalPairs.has(f)) {
errors.push(
`"${f}" is a member subblock ID inside a canonical pair — use the canonicalParamId instead`
)
continue
}
if (!allSubBlockIds.has(f)) {
errors.push(`"${f}" is not a canonicalParamId or subblock ID in any block definition`)
}
}
if (errors.length > 0) {
throw new Error(`SELECTOR_CONTEXT_FIELDS validation failed:\n${errors.join('\n')}`)
}
})
})

View File

@@ -0,0 +1,60 @@
import { getBlock } from '@/blocks'
import type { SelectorContext } from '@/hooks/selectors/types'
import type { SubBlockState } from '@/stores/workflows/workflow/types'
import { buildCanonicalIndex } from './visibility'
/**
* Canonical param IDs (or raw subblock IDs) that correspond to SelectorContext fields.
* A subblock's resolved canonical key is set on the context only if it appears here.
*/
export const SELECTOR_CONTEXT_FIELDS = new Set<keyof SelectorContext>([
'oauthCredential',
'domain',
'teamId',
'projectId',
'knowledgeBaseId',
'planId',
'siteId',
'collectionId',
'spreadsheetId',
'fileId',
'baseId',
'datasetId',
'serviceDeskId',
])
/**
* Builds a SelectorContext from a block's subBlocks using the canonical index.
*
* Iterates all subblocks, resolves each through canonicalIdBySubBlockId to get
* the canonical key, then checks it against SELECTOR_CONTEXT_FIELDS.
* This avoids hardcoding subblock IDs and automatically handles basic/advanced
* renames.
*/
export function buildSelectorContextFromBlock(
blockType: string,
subBlocks: Record<string, SubBlockState | { value?: unknown }>,
opts?: { workflowId?: string }
): SelectorContext {
const context: SelectorContext = {}
if (opts?.workflowId) context.workflowId = opts.workflowId
const blockConfig = getBlock(blockType)
if (!blockConfig) return context
const canonicalIndex = buildCanonicalIndex(blockConfig.subBlocks)
for (const [subBlockId, subBlock] of Object.entries(subBlocks)) {
const val = subBlock?.value
if (val === null || val === undefined) continue
const strValue = typeof val === 'string' ? val : String(val)
if (!strValue) continue
const canonicalKey = canonicalIndex.canonicalIdBySubBlockId[subBlockId] ?? subBlockId
if (SELECTOR_CONTEXT_FIELDS.has(canonicalKey as keyof SelectorContext)) {
context[canonicalKey as keyof SelectorContext] = strValue
}
}
return context
}

View File

@@ -17,6 +17,7 @@ import { jiraGetWorklogsTool } from '@/tools/jira/get_worklogs'
import { jiraRemoveWatcherTool } from '@/tools/jira/remove_watcher'
import { jiraRetrieveTool } from '@/tools/jira/retrieve'
import { jiraSearchIssuesTool } from '@/tools/jira/search_issues'
import { jiraSearchUsersTool } from '@/tools/jira/search_users'
import { jiraTransitionIssueTool } from '@/tools/jira/transition_issue'
import { jiraUpdateTool } from '@/tools/jira/update'
import { jiraUpdateCommentTool } from '@/tools/jira/update_comment'
@@ -48,4 +49,5 @@ export {
jiraAddWatcherTool,
jiraRemoveWatcherTool,
jiraGetUsersTool,
jiraSearchUsersTool,
}

View File

@@ -0,0 +1,166 @@
import type { JiraSearchUsersParams, JiraSearchUsersResponse } from '@/tools/jira/types'
import { TIMESTAMP_OUTPUT, USER_OUTPUT_PROPERTIES } from '@/tools/jira/types'
import { getJiraCloudId, transformUser } from '@/tools/jira/utils'
import type { ToolConfig } from '@/tools/types'
export const jiraSearchUsersTool: ToolConfig<JiraSearchUsersParams, JiraSearchUsersResponse> = {
id: 'jira_search_users',
name: 'Jira Search Users',
description:
'Search for Jira users by email address or display name. Returns matching users with their accountId, displayName, and emailAddress.',
version: '1.0.0',
oauth: {
required: true,
provider: 'jira',
},
params: {
accessToken: {
type: 'string',
required: true,
visibility: 'hidden',
description: 'OAuth access token for Jira',
},
domain: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Your Jira domain (e.g., yourcompany.atlassian.net)',
},
query: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description:
'A query string to search for users. Can be an email address, display name, or partial match.',
},
maxResults: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description: 'Maximum number of users to return (default: 50, max: 1000)',
},
startAt: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description: 'The index of the first user to return (for pagination, default: 0)',
},
cloudId: {
type: 'string',
required: false,
visibility: 'hidden',
description:
'Jira Cloud ID for the instance. If not provided, it will be fetched using the domain.',
},
},
request: {
url: (params: JiraSearchUsersParams) => {
if (params.cloudId) {
const queryParams = new URLSearchParams()
queryParams.append('query', params.query)
if (params.maxResults !== undefined)
queryParams.append('maxResults', String(params.maxResults))
if (params.startAt !== undefined) queryParams.append('startAt', String(params.startAt))
return `https://api.atlassian.com/ex/jira/${params.cloudId}/rest/api/3/user/search?${queryParams.toString()}`
}
return 'https://api.atlassian.com/oauth/token/accessible-resources'
},
method: 'GET',
headers: (params: JiraSearchUsersParams) => ({
Accept: 'application/json',
Authorization: `Bearer ${params.accessToken}`,
}),
},
transformResponse: async (response: Response, params?: JiraSearchUsersParams) => {
const fetchUsers = async (cloudId: string) => {
const queryParams = new URLSearchParams()
queryParams.append('query', params!.query)
if (params!.maxResults !== undefined)
queryParams.append('maxResults', String(params!.maxResults))
if (params!.startAt !== undefined) queryParams.append('startAt', String(params!.startAt))
const usersUrl = `https://api.atlassian.com/ex/jira/${cloudId}/rest/api/3/user/search?${queryParams.toString()}`
const usersResponse = await fetch(usersUrl, {
method: 'GET',
headers: {
Accept: 'application/json',
Authorization: `Bearer ${params!.accessToken}`,
},
})
if (!usersResponse.ok) {
let message = `Failed to search Jira users (${usersResponse.status})`
try {
const err = await usersResponse.json()
message = err?.errorMessages?.join(', ') || err?.message || message
} catch (_e) {}
throw new Error(message)
}
return usersResponse.json()
}
let data: any
if (!params?.cloudId) {
const cloudId = await getJiraCloudId(params!.domain, params!.accessToken)
data = await fetchUsers(cloudId)
} else {
if (!response.ok) {
let message = `Failed to search Jira users (${response.status})`
try {
const err = await response.json()
message = err?.errorMessages?.join(', ') || err?.message || message
} catch (_e) {}
throw new Error(message)
}
data = await response.json()
}
const users = Array.isArray(data) ? data.filter(Boolean) : []
return {
success: true,
output: {
ts: new Date().toISOString(),
users: users.map((user: any) => ({
...(transformUser(user) ?? { accountId: '', displayName: '' }),
self: user.self ?? null,
})),
total: users.length,
startAt: params?.startAt ?? 0,
maxResults: params?.maxResults ?? 50,
},
}
},
outputs: {
ts: TIMESTAMP_OUTPUT,
users: {
type: 'array',
description: 'Array of matching Jira users',
items: {
type: 'object',
properties: {
...USER_OUTPUT_PROPERTIES,
self: {
type: 'string',
description: 'REST API URL for this user',
optional: true,
},
},
},
},
total: {
type: 'number',
description: 'Number of users returned in this page (may be less than total matches)',
},
startAt: { type: 'number', description: 'Pagination start index' },
maxResults: { type: 'number', description: 'Maximum results per page' },
},
}

View File

@@ -1549,6 +1549,34 @@ export interface JiraGetUsersParams {
cloudId?: string
}
export interface JiraSearchUsersParams {
accessToken: string
domain: string
query: string
maxResults?: number
startAt?: number
cloudId?: string
}
export interface JiraSearchUsersResponse extends ToolResponse {
output: {
ts: string
users: Array<{
accountId: string
accountType?: string | null
active?: boolean | null
displayName: string
emailAddress?: string | null
avatarUrl?: string | null
timeZone?: string | null
self?: string | null
}>
total: number
startAt: number
maxResults: number
}
}
export interface JiraGetUsersResponse extends ToolResponse {
output: {
ts: string
@@ -1594,3 +1622,4 @@ export type JiraResponse =
| JiraAddWatcherResponse
| JiraRemoveWatcherResponse
| JiraGetUsersResponse
| JiraSearchUsersResponse

View File

@@ -1085,6 +1085,7 @@ import {
jiraRemoveWatcherTool,
jiraRetrieveTool,
jiraSearchIssuesTool,
jiraSearchUsersTool,
jiraTransitionIssueTool,
jiraUpdateCommentTool,
jiraUpdateTool,
@@ -2536,6 +2537,7 @@ export const tools: Record<string, ToolConfig> = {
jira_add_watcher: jiraAddWatcherTool,
jira_remove_watcher: jiraRemoveWatcherTool,
jira_get_users: jiraGetUsersTool,
jira_search_users: jiraSearchUsersTool,
jsm_get_service_desks: jsmGetServiceDesksTool,
jsm_get_request_types: jsmGetRequestTypesTool,
jsm_get_request_type_fields: jsmGetRequestTypeFieldsTool,