mirror of
https://github.com/simstudioai/sim.git
synced 2026-04-06 03:00:16 -04:00
Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ff7b5b528c | ||
|
|
cef321bda2 | ||
|
|
1809b3801b | ||
|
|
bc111a6d5c | ||
|
|
12908c14be | ||
|
|
638063cac1 | ||
|
|
5f7a980c5f | ||
|
|
a2c08e19a8 |
@@ -1146,6 +1146,25 @@ export function DevinIcon(props: SVGProps<SVGSVGElement>) {
|
||||
)
|
||||
}
|
||||
|
||||
export function DocuSignIcon(props: SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
<svg {...props} viewBox='0 0 1547 1549' xmlns='http://www.w3.org/2000/svg'>
|
||||
<path
|
||||
d='m1113.4 1114.9v395.6c0 20.8-16.7 37.6-37.5 37.6h-1038.4c-20.7 0-37.5-16.8-37.5-37.6v-1039c0-20.7 16.8-37.5 37.5-37.5h394.3v643.4c0 20.7 16.8 37.5 37.5 37.5z'
|
||||
fill='#4c00ff'
|
||||
/>
|
||||
<path
|
||||
d='m1546 557.1c0 332.4-193.9 557-432.6 557.8v-418.8c0-12-4.8-24-13.5-31.9l-217.1-217.4c-8.8-8.8-20-13.6-32-13.6h-418.2v-394.8c0-20.8 16.8-37.6 37.5-37.6h585.1c277.7-0.8 490.8 223 490.8 556.3z'
|
||||
fill='#ff5252'
|
||||
/>
|
||||
<path
|
||||
d='m1099.9 663.4c8.7 8.7 13.5 19.9 13.5 31.9v418.8h-643.3c-20.7 0-37.5-16.8-37.5-37.5v-643.4h418.2c12 0 24 4.8 32 13.6z'
|
||||
fill='#000000'
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
export function DiscordIcon(props: SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
<svg
|
||||
@@ -1390,7 +1409,7 @@ export function AmplitudeIcon(props: SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
<svg {...props} xmlns='http://www.w3.org/2000/svg' viewBox='0 0 49 49'>
|
||||
<path
|
||||
fill='#FFFFFF'
|
||||
fill='currentColor'
|
||||
d='M23.4,15.3c0.6,1.8,1.2,4.1,1.9,6.7c-2.6,0-5.3-0.1-7.8-0.1h-1.3c1.5-5.7,3.2-10.1,4.6-11.1 c0.1-0.1,0.2-0.1,0.4-0.1c0.2,0,0.3,0.1,0.5,0.3C21.9,11.5,22.5,12.7,23.4,15.3z M49,24.5C49,38,38,49,24.5,49S0,38,0,24.5 S11,0,24.5,0S49,11,49,24.5z M42.7,23.9c0-0.6-0.4-1.2-1-1.3l0,0l0,0l0,0c-0.1,0-0.1,0-0.2,0h-0.2c-4.1-0.3-8.4-0.4-12.4-0.5l0,0 C27,14.8,24.5,7.4,21.3,7.4c-3,0-5.8,4.9-8.2,14.5c-1.7,0-3.2,0-4.6-0.1c-0.1,0-0.2,0-0.2,0c-0.3,0-0.5,0-0.5,0 c-0.8,0.1-1.4,0.9-1.4,1.7c0,0.8,0.6,1.6,1.5,1.7l0,0h4.6c-0.4,1.9-0.8,3.8-1.1,5.6l-0.1,0.8l0,0c0,0.6,0.5,1.1,1.1,1.1 c0.4,0,0.8-0.2,1-0.5l0,0l2.2-7.1h10.7c0.8,3.1,1.7,6.3,2.8,9.3c0.6,1.6,2,5.4,4.4,5.4l0,0c3.6,0,5-5.8,5.9-9.6 c0.2-0.8,0.4-1.5,0.5-2.1l0.1-0.2l0,0c0-0.1,0-0.2,0-0.3c-0.1-0.2-0.2-0.3-0.4-0.4c-0.3-0.1-0.5,0.1-0.6,0.4l0,0l-0.1,0.2 c-0.3,0.8-0.6,1.6-0.8,2.3v0.1c-1.6,4.4-2.3,6.4-3.7,6.4l0,0l0,0l0,0c-1.8,0-3.5-7.3-4.1-10.1c-0.1-0.5-0.2-0.9-0.3-1.3h11.7 c0.2,0,0.4-0.1,0.6-0.1l0,0c0,0,0,0,0.1,0c0,0,0,0,0.1,0l0,0c0,0,0.1,0,0.1-0.1l0,0C42.5,24.6,42.7,24.3,42.7,23.9z'
|
||||
/>
|
||||
</svg>
|
||||
@@ -4569,11 +4588,17 @@ export function ShopifyIcon(props: SVGProps<SVGSVGElement>) {
|
||||
|
||||
export function BoxCompanyIcon(props: SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
<svg {...props} xmlns='http://www.w3.org/2000/svg' viewBox='0 0 41 22'>
|
||||
<path
|
||||
d='M39.7 19.2c.5.7.4 1.6-.2 2.1-.7.5-1.7.4-2.2-.2l-3.5-4.5-3.4 4.4c-.5.7-1.5.7-2.2.2-.7-.5-.8-1.4-.3-2.1l4-5.2-4-5.2c-.5-.7-.3-1.7.3-2.2.7-.5 1.7-.3 2.2.3l3.4 4.5L37.3 7c.5-.7 1.4-.8 2.2-.3.7.5.7 1.5.2 2.2L35.8 14l3.9 5.2zm-18.2-.6c-2.6 0-4.7-2-4.7-4.6 0-2.5 2.1-4.6 4.7-4.6s4.7 2.1 4.7 4.6c-.1 2.6-2.2 4.6-4.7 4.6zm-13.8 0c-2.6 0-4.7-2-4.7-4.6 0-2.5 2.1-4.6 4.7-4.6s4.7 2.1 4.7 4.6c0 2.6-2.1 4.6-4.7 4.6zM21.5 6.4c-2.9 0-5.5 1.6-6.8 4-1.3-2.4-3.9-4-6.9-4-1.8 0-3.4.6-4.7 1.5V1.5C3.1.7 2.4 0 1.6 0 .7 0 0 .7 0 1.5v12.6c.1 4.2 3.5 7.5 7.7 7.5 3 0 5.6-1.7 6.9-4.1 1.3 2.4 3.9 4.1 6.8 4.1 4.3 0 7.8-3.4 7.8-7.7.1-4.1-3.4-7.5-7.7-7.5z'
|
||||
fill='currentColor'
|
||||
/>
|
||||
<svg
|
||||
{...props}
|
||||
xmlns='http://www.w3.org/2000/svg'
|
||||
width='2500'
|
||||
height='1379'
|
||||
viewBox='0 0 444.893 245.414'
|
||||
>
|
||||
<g fill='#0075C9'>
|
||||
<path d='M239.038 72.43c-33.081 0-61.806 18.6-76.322 45.904-14.516-27.305-43.24-45.902-76.32-45.902-19.443 0-37.385 6.424-51.821 17.266V16.925h-.008C34.365 7.547 26.713 0 17.286 0 7.858 0 .208 7.547.008 16.925H0v143.333h.036c.768 47.051 39.125 84.967 86.359 84.967 33.08 0 61.805-18.603 76.32-45.908 14.517 27.307 43.241 45.906 76.321 45.906 47.715 0 86.396-38.684 86.396-86.396.001-47.718-38.682-86.397-86.394-86.397zM86.395 210.648c-28.621 0-51.821-23.201-51.821-51.82 0-28.623 23.201-51.823 51.821-51.823 28.621 0 51.822 23.2 51.822 51.823 0 28.619-23.201 51.82-51.822 51.82zm152.643 0c-28.622 0-51.821-23.201-51.821-51.822 0-28.623 23.2-51.821 51.821-51.821 28.619 0 51.822 23.198 51.822 51.821-.001 28.621-23.203 51.822-51.822 51.822z' />
|
||||
<path d='M441.651 218.033l-44.246-59.143 44.246-59.144-.008-.007c5.473-7.62 3.887-18.249-3.652-23.913-7.537-5.658-18.187-4.221-23.98 3.157l-.004-.002-38.188 51.047-38.188-51.047-.006.009c-5.793-7.385-16.441-8.822-23.981-3.16-7.539 5.664-9.125 16.293-3.649 23.911l-.008.005 44.245 59.144-44.245 59.143.008.005c-5.477 7.62-3.89 18.247 3.649 23.909 7.54 5.664 18.188 4.225 23.981-3.155l.006.007 38.188-51.049 38.188 51.049.004-.002c5.794 7.377 16.443 8.814 23.98 3.154 7.539-5.662 9.125-16.291 3.652-23.91l.008-.008z' />
|
||||
</g>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@ import {
|
||||
AsanaIcon,
|
||||
AshbyIcon,
|
||||
AttioIcon,
|
||||
BoxCompanyIcon,
|
||||
BrainIcon,
|
||||
BrandfetchIcon,
|
||||
BrowserUseIcon,
|
||||
@@ -32,6 +33,7 @@ import {
|
||||
DevinIcon,
|
||||
DiscordIcon,
|
||||
DocumentIcon,
|
||||
DocuSignIcon,
|
||||
DropboxIcon,
|
||||
DsPyIcon,
|
||||
DubIcon,
|
||||
@@ -184,6 +186,7 @@ export const blockTypeToIconMap: Record<string, IconComponent> = {
|
||||
asana: AsanaIcon,
|
||||
ashby: AshbyIcon,
|
||||
attio: AttioIcon,
|
||||
box: BoxCompanyIcon,
|
||||
brandfetch: BrandfetchIcon,
|
||||
browser_use: BrowserUseIcon,
|
||||
calcom: CalComIcon,
|
||||
@@ -198,6 +201,7 @@ export const blockTypeToIconMap: Record<string, IconComponent> = {
|
||||
datadog: DatadogIcon,
|
||||
devin: DevinIcon,
|
||||
discord: DiscordIcon,
|
||||
docusign: DocuSignIcon,
|
||||
dropbox: DropboxIcon,
|
||||
dspy: DsPyIcon,
|
||||
dub: DubIcon,
|
||||
|
||||
440
apps/docs/content/docs/en/tools/box.mdx
Normal file
440
apps/docs/content/docs/en/tools/box.mdx
Normal file
@@ -0,0 +1,440 @@
|
||||
---
|
||||
title: Box
|
||||
description: Manage files, folders, and e-signatures with Box
|
||||
---
|
||||
|
||||
import { BlockInfoCard } from "@/components/ui/block-info-card"
|
||||
|
||||
<BlockInfoCard
|
||||
type="box"
|
||||
color="#FFFFFF"
|
||||
/>
|
||||
|
||||
{/* MANUAL-CONTENT-START:intro */}
|
||||
[Box](https://www.box.com/) is a leading cloud content management and file sharing platform trusted by enterprises worldwide to securely store, manage, and collaborate on files. Box provides robust APIs for automating file operations and integrating with business workflows, including [Box Sign](https://www.box.com/esignature) for native e-signatures.
|
||||
|
||||
With the Box integration in Sim, you can:
|
||||
|
||||
- **Upload files**: Upload documents, images, and other files to any Box folder
|
||||
- **Download files**: Retrieve file content from Box for processing in your workflows
|
||||
- **Get file info**: Access detailed metadata including size, owner, timestamps, tags, and shared links
|
||||
- **List folder contents**: Browse files and folders with sorting and pagination support
|
||||
- **Create folders**: Organize your Box storage by creating new folders programmatically
|
||||
- **Delete files and folders**: Remove content with optional recursive deletion for folders
|
||||
- **Copy files**: Duplicate files across folders with optional renaming
|
||||
- **Search**: Find files and folders by name, content, extension, or location
|
||||
- **Update file metadata**: Rename, move, add descriptions, or tag files
|
||||
- **Create sign requests**: Send documents for e-signature with one or more signers
|
||||
- **Track signing status**: Monitor the progress of sign requests
|
||||
- **List sign requests**: View all sign requests with marker-based pagination
|
||||
- **Cancel sign requests**: Cancel pending sign requests that are no longer needed
|
||||
- **Resend sign reminders**: Send reminder notifications to signers who haven't completed signing
|
||||
|
||||
These capabilities allow your Sim agents to automate Box operations directly within your workflows — from organizing documents and distributing content to processing uploaded files, managing e-signature workflows for offer letters and contracts, and maintaining structured cloud storage as part of your business processes.
|
||||
{/* MANUAL-CONTENT-END */}
|
||||
|
||||
|
||||
## Usage Instructions
|
||||
|
||||
Integrate Box into your workflow to manage files, folders, and e-signatures. Upload and download files, search content, create folders, send documents for e-signature, track signing status, and more.
|
||||
|
||||
|
||||
|
||||
## Tools
|
||||
|
||||
### `box_upload_file`
|
||||
|
||||
Upload a file to a Box folder
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `parentFolderId` | string | Yes | The ID of the folder to upload the file to \(use "0" for root\) |
|
||||
| `file` | file | No | The file to upload \(UserFile object\) |
|
||||
| `fileContent` | string | No | Legacy: base64 encoded file content |
|
||||
| `fileName` | string | No | Optional filename override |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `id` | string | File ID |
|
||||
| `name` | string | File name |
|
||||
| `size` | number | File size in bytes |
|
||||
| `sha1` | string | SHA1 hash of file content |
|
||||
| `createdAt` | string | Creation timestamp |
|
||||
| `modifiedAt` | string | Last modified timestamp |
|
||||
| `parentId` | string | Parent folder ID |
|
||||
| `parentName` | string | Parent folder name |
|
||||
|
||||
### `box_download_file`
|
||||
|
||||
Download a file from Box
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `fileId` | string | Yes | The ID of the file to download |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `file` | file | Downloaded file stored in execution files |
|
||||
| `content` | string | Base64 encoded file content |
|
||||
|
||||
### `box_get_file_info`
|
||||
|
||||
Get detailed information about a file in Box
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `fileId` | string | Yes | The ID of the file to get information about |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `id` | string | File ID |
|
||||
| `name` | string | File name |
|
||||
| `description` | string | File description |
|
||||
| `size` | number | File size in bytes |
|
||||
| `sha1` | string | SHA1 hash of file content |
|
||||
| `createdAt` | string | Creation timestamp |
|
||||
| `modifiedAt` | string | Last modified timestamp |
|
||||
| `createdBy` | object | User who created the file |
|
||||
| `modifiedBy` | object | User who last modified the file |
|
||||
| `ownedBy` | object | User who owns the file |
|
||||
| `parentId` | string | Parent folder ID |
|
||||
| `parentName` | string | Parent folder name |
|
||||
| `sharedLink` | json | Shared link details |
|
||||
| `tags` | array | File tags |
|
||||
| `commentCount` | number | Number of comments |
|
||||
|
||||
### `box_list_folder_items`
|
||||
|
||||
List files and folders in a Box folder
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `folderId` | string | Yes | The ID of the folder to list items from \(use "0" for root\) |
|
||||
| `limit` | number | No | Maximum number of items to return per page |
|
||||
| `offset` | number | No | The offset for pagination |
|
||||
| `sort` | string | No | Sort field: id, name, date, or size |
|
||||
| `direction` | string | No | Sort direction: ASC or DESC |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `entries` | array | List of items in the folder |
|
||||
| ↳ `type` | string | Item type \(file, folder, web_link\) |
|
||||
| ↳ `id` | string | Item ID |
|
||||
| ↳ `name` | string | Item name |
|
||||
| ↳ `size` | number | Item size in bytes |
|
||||
| ↳ `createdAt` | string | Creation timestamp |
|
||||
| ↳ `modifiedAt` | string | Last modified timestamp |
|
||||
| `totalCount` | number | Total number of items in the folder |
|
||||
| `offset` | number | Current pagination offset |
|
||||
| `limit` | number | Current pagination limit |
|
||||
|
||||
### `box_create_folder`
|
||||
|
||||
Create a new folder in Box
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `name` | string | Yes | Name for the new folder |
|
||||
| `parentFolderId` | string | Yes | The ID of the parent folder \(use "0" for root\) |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `id` | string | Folder ID |
|
||||
| `name` | string | Folder name |
|
||||
| `createdAt` | string | Creation timestamp |
|
||||
| `modifiedAt` | string | Last modified timestamp |
|
||||
| `parentId` | string | Parent folder ID |
|
||||
| `parentName` | string | Parent folder name |
|
||||
|
||||
### `box_delete_file`
|
||||
|
||||
Delete a file from Box
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `fileId` | string | Yes | The ID of the file to delete |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `deleted` | boolean | Whether the file was successfully deleted |
|
||||
| `message` | string | Success confirmation message |
|
||||
|
||||
### `box_delete_folder`
|
||||
|
||||
Delete a folder from Box
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `folderId` | string | Yes | The ID of the folder to delete |
|
||||
| `recursive` | boolean | No | Delete folder and all its contents recursively |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `deleted` | boolean | Whether the folder was successfully deleted |
|
||||
| `message` | string | Success confirmation message |
|
||||
|
||||
### `box_copy_file`
|
||||
|
||||
Copy a file to another folder in Box
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `fileId` | string | Yes | The ID of the file to copy |
|
||||
| `parentFolderId` | string | Yes | The ID of the destination folder |
|
||||
| `name` | string | No | Optional new name for the copied file |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `id` | string | File ID |
|
||||
| `name` | string | File name |
|
||||
| `size` | number | File size in bytes |
|
||||
| `sha1` | string | SHA1 hash of file content |
|
||||
| `createdAt` | string | Creation timestamp |
|
||||
| `modifiedAt` | string | Last modified timestamp |
|
||||
| `parentId` | string | Parent folder ID |
|
||||
| `parentName` | string | Parent folder name |
|
||||
|
||||
### `box_search`
|
||||
|
||||
Search for files and folders in Box
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `query` | string | Yes | The search query string |
|
||||
| `limit` | number | No | Maximum number of results to return |
|
||||
| `offset` | number | No | The offset for pagination |
|
||||
| `ancestorFolderId` | string | No | Restrict search to a specific folder and its subfolders |
|
||||
| `fileExtensions` | string | No | Comma-separated file extensions to filter by \(e.g., pdf,docx\) |
|
||||
| `type` | string | No | Restrict to a specific content type: file, folder, or web_link |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `results` | array | Search results |
|
||||
| ↳ `type` | string | Item type \(file, folder, web_link\) |
|
||||
| ↳ `id` | string | Item ID |
|
||||
| ↳ `name` | string | Item name |
|
||||
| ↳ `size` | number | Item size in bytes |
|
||||
| ↳ `createdAt` | string | Creation timestamp |
|
||||
| ↳ `modifiedAt` | string | Last modified timestamp |
|
||||
| ↳ `parentId` | string | Parent folder ID |
|
||||
| ↳ `parentName` | string | Parent folder name |
|
||||
| `totalCount` | number | Total number of matching results |
|
||||
|
||||
### `box_update_file`
|
||||
|
||||
Update file info in Box (rename, move, change description, add tags)
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `fileId` | string | Yes | The ID of the file to update |
|
||||
| `name` | string | No | New name for the file |
|
||||
| `description` | string | No | New description for the file \(max 256 characters\) |
|
||||
| `parentFolderId` | string | No | Move the file to a different folder by specifying the folder ID |
|
||||
| `tags` | string | No | Comma-separated tags to set on the file |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `id` | string | File ID |
|
||||
| `name` | string | File name |
|
||||
| `description` | string | File description |
|
||||
| `size` | number | File size in bytes |
|
||||
| `sha1` | string | SHA1 hash of file content |
|
||||
| `createdAt` | string | Creation timestamp |
|
||||
| `modifiedAt` | string | Last modified timestamp |
|
||||
| `createdBy` | object | User who created the file |
|
||||
| `modifiedBy` | object | User who last modified the file |
|
||||
| `ownedBy` | object | User who owns the file |
|
||||
| `parentId` | string | Parent folder ID |
|
||||
| `parentName` | string | Parent folder name |
|
||||
| `sharedLink` | json | Shared link details |
|
||||
| `tags` | array | File tags |
|
||||
| `commentCount` | number | Number of comments |
|
||||
|
||||
### `box_sign_create_request`
|
||||
|
||||
Create a new Box Sign request to send documents for e-signature
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `sourceFileIds` | string | Yes | Comma-separated Box file IDs to send for signing |
|
||||
| `signerEmail` | string | Yes | Primary signer email address |
|
||||
| `signerRole` | string | No | Primary signer role: signer, approver, or final_copy_reader \(default: signer\) |
|
||||
| `additionalSigners` | string | No | JSON array of additional signers, e.g. \[\{"email":"user@example.com","role":"signer"\}\] |
|
||||
| `parentFolderId` | string | No | Box folder ID where signed documents will be stored \(default: user root\) |
|
||||
| `emailSubject` | string | No | Custom subject line for the signing email |
|
||||
| `emailMessage` | string | No | Custom message in the signing email body |
|
||||
| `name` | string | No | Name for the sign request |
|
||||
| `daysValid` | number | No | Number of days before the request expires \(0-730\) |
|
||||
| `areRemindersEnabled` | boolean | No | Whether to send automatic signing reminders |
|
||||
| `areTextSignaturesEnabled` | boolean | No | Whether to allow typed \(text\) signatures |
|
||||
| `signatureColor` | string | No | Signature color: blue, black, or red |
|
||||
| `redirectUrl` | string | No | URL to redirect signers to after signing |
|
||||
| `declinedRedirectUrl` | string | No | URL to redirect signers to after declining |
|
||||
| `isDocumentPreparationNeeded` | boolean | No | Whether document preparation is needed before sending |
|
||||
| `externalId` | string | No | External system reference ID |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `id` | string | Sign request ID |
|
||||
| `status` | string | Request status \(converting, created, sent, viewed, signed, cancelled, declined, expired, error_converting, error_sending, finalizing, error_finalizing\) |
|
||||
| `name` | string | Sign request name |
|
||||
| `shortId` | string | Human-readable short ID |
|
||||
| `signers` | array | List of signers |
|
||||
| `sourceFiles` | array | Source files for signing |
|
||||
| `emailSubject` | string | Custom email subject line |
|
||||
| `emailMessage` | string | Custom email message body |
|
||||
| `daysValid` | number | Number of days the request is valid |
|
||||
| `createdAt` | string | Creation timestamp |
|
||||
| `autoExpireAt` | string | Auto-expiration timestamp |
|
||||
| `prepareUrl` | string | URL for document preparation \(if preparation is needed\) |
|
||||
| `senderEmail` | string | Email of the sender |
|
||||
|
||||
### `box_sign_get_request`
|
||||
|
||||
Get the details and status of a Box Sign request
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `signRequestId` | string | Yes | The ID of the sign request to retrieve |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `id` | string | Sign request ID |
|
||||
| `status` | string | Request status \(converting, created, sent, viewed, signed, cancelled, declined, expired, error_converting, error_sending, finalizing, error_finalizing\) |
|
||||
| `name` | string | Sign request name |
|
||||
| `shortId` | string | Human-readable short ID |
|
||||
| `signers` | array | List of signers |
|
||||
| `sourceFiles` | array | Source files for signing |
|
||||
| `emailSubject` | string | Custom email subject line |
|
||||
| `emailMessage` | string | Custom email message body |
|
||||
| `daysValid` | number | Number of days the request is valid |
|
||||
| `createdAt` | string | Creation timestamp |
|
||||
| `autoExpireAt` | string | Auto-expiration timestamp |
|
||||
| `prepareUrl` | string | URL for document preparation \(if preparation is needed\) |
|
||||
| `senderEmail` | string | Email of the sender |
|
||||
|
||||
### `box_sign_list_requests`
|
||||
|
||||
List all Box Sign requests
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `limit` | number | No | Maximum number of sign requests to return \(max 1000\) |
|
||||
| `marker` | string | No | Pagination marker from a previous response |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `signRequests` | array | List of sign requests |
|
||||
| ↳ `id` | string | Sign request ID |
|
||||
| ↳ `status` | string | Request status \(converting, created, sent, viewed, signed, cancelled, declined, expired, error_converting, error_sending, finalizing, error_finalizing\) |
|
||||
| ↳ `name` | string | Sign request name |
|
||||
| ↳ `shortId` | string | Human-readable short ID |
|
||||
| ↳ `signers` | array | List of signers |
|
||||
| ↳ `sourceFiles` | array | Source files for signing |
|
||||
| ↳ `emailSubject` | string | Custom email subject line |
|
||||
| ↳ `emailMessage` | string | Custom email message body |
|
||||
| ↳ `daysValid` | number | Number of days the request is valid |
|
||||
| ↳ `createdAt` | string | Creation timestamp |
|
||||
| ↳ `autoExpireAt` | string | Auto-expiration timestamp |
|
||||
| ↳ `prepareUrl` | string | URL for document preparation \(if preparation is needed\) |
|
||||
| ↳ `senderEmail` | string | Email of the sender |
|
||||
| `count` | number | Number of sign requests returned in this page |
|
||||
| `nextMarker` | string | Marker for next page of results |
|
||||
|
||||
### `box_sign_cancel_request`
|
||||
|
||||
Cancel a pending Box Sign request
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `signRequestId` | string | Yes | The ID of the sign request to cancel |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `id` | string | Sign request ID |
|
||||
| `status` | string | Request status \(converting, created, sent, viewed, signed, cancelled, declined, expired, error_converting, error_sending, finalizing, error_finalizing\) |
|
||||
| `name` | string | Sign request name |
|
||||
| `shortId` | string | Human-readable short ID |
|
||||
| `signers` | array | List of signers |
|
||||
| `sourceFiles` | array | Source files for signing |
|
||||
| `emailSubject` | string | Custom email subject line |
|
||||
| `emailMessage` | string | Custom email message body |
|
||||
| `daysValid` | number | Number of days the request is valid |
|
||||
| `createdAt` | string | Creation timestamp |
|
||||
| `autoExpireAt` | string | Auto-expiration timestamp |
|
||||
| `prepareUrl` | string | URL for document preparation \(if preparation is needed\) |
|
||||
| `senderEmail` | string | Email of the sender |
|
||||
|
||||
### `box_sign_resend_request`
|
||||
|
||||
Resend a Box Sign request to signers who have not yet signed
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `signRequestId` | string | Yes | The ID of the sign request to resend |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `message` | string | Success confirmation message |
|
||||
|
||||
|
||||
230
apps/docs/content/docs/en/tools/docusign.mdx
Normal file
230
apps/docs/content/docs/en/tools/docusign.mdx
Normal file
@@ -0,0 +1,230 @@
|
||||
---
|
||||
title: DocuSign
|
||||
description: Send documents for e-signature via DocuSign
|
||||
---
|
||||
|
||||
import { BlockInfoCard } from "@/components/ui/block-info-card"
|
||||
|
||||
<BlockInfoCard
|
||||
type="docusign"
|
||||
color="#FFFFFF"
|
||||
/>
|
||||
|
||||
{/* MANUAL-CONTENT-START:intro */}
|
||||
[DocuSign](https://www.docusign.com) is the world's leading e-signature platform, enabling businesses to send, sign, and manage agreements digitally. With its powerful eSignature REST API, DocuSign supports the full document lifecycle from creation through completion.
|
||||
|
||||
With the DocuSign integration in Sim, you can:
|
||||
|
||||
- **Send envelopes**: Create and send documents for e-signature with custom recipients and signing tabs
|
||||
- **Use templates**: Send envelopes from pre-configured DocuSign templates with role assignments
|
||||
- **Track status**: Get envelope details including signing progress, timestamps, and recipient status
|
||||
- **List envelopes**: Search and filter envelopes by date range, status, and text
|
||||
- **Download documents**: Retrieve signed documents as base64-encoded files
|
||||
- **Manage recipients**: View signer and CC recipient details and signing status
|
||||
- **Void envelopes**: Cancel in-progress envelopes with a reason
|
||||
|
||||
In Sim, the DocuSign integration enables your agents to automate document workflows end-to-end. Agents can generate agreements, send them for signature, monitor completion, and retrieve signed copies—powering contract management, HR onboarding, sales closings, and compliance processes.
|
||||
{/* MANUAL-CONTENT-END */}
|
||||
|
||||
|
||||
## Usage Instructions
|
||||
|
||||
Create and send envelopes for e-signature, use templates, check signing status, download signed documents, and manage recipients with DocuSign.
|
||||
|
||||
|
||||
|
||||
## Tools
|
||||
|
||||
### `docusign_send_envelope`
|
||||
|
||||
Create and send a DocuSign envelope with a document for e-signature
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `emailSubject` | string | Yes | Email subject for the envelope |
|
||||
| `emailBody` | string | No | Email body message |
|
||||
| `signerEmail` | string | Yes | Email address of the signer |
|
||||
| `signerName` | string | Yes | Full name of the signer |
|
||||
| `ccEmail` | string | No | Email address of carbon copy recipient |
|
||||
| `ccName` | string | No | Full name of carbon copy recipient |
|
||||
| `file` | file | No | Document file to send for signature |
|
||||
| `status` | string | No | Envelope status: "sent" to send immediately, "created" for draft \(default: "sent"\) |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `envelopeId` | string | Created envelope ID |
|
||||
| `status` | string | Envelope status |
|
||||
| `statusDateTime` | string | Status change datetime |
|
||||
| `uri` | string | Envelope URI |
|
||||
|
||||
### `docusign_create_from_template`
|
||||
|
||||
Create and send a DocuSign envelope using a pre-built template
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `templateId` | string | Yes | DocuSign template ID to use |
|
||||
| `emailSubject` | string | No | Override email subject \(uses template default if not set\) |
|
||||
| `emailBody` | string | No | Override email body message |
|
||||
| `templateRoles` | string | Yes | JSON array of template roles, e.g. \[\{"roleName":"Signer","name":"John","email":"john@example.com"\}\] |
|
||||
| `status` | string | No | Envelope status: "sent" to send immediately, "created" for draft \(default: "sent"\) |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `envelopeId` | string | Created envelope ID |
|
||||
| `status` | string | Envelope status |
|
||||
| `statusDateTime` | string | Status change datetime |
|
||||
| `uri` | string | Envelope URI |
|
||||
|
||||
### `docusign_get_envelope`
|
||||
|
||||
Get the details and status of a DocuSign envelope
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `envelopeId` | string | Yes | The envelope ID to retrieve |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `envelopeId` | string | Envelope ID |
|
||||
| `status` | string | Envelope status \(created, sent, delivered, completed, declined, voided\) |
|
||||
| `emailSubject` | string | Email subject line |
|
||||
| `sentDateTime` | string | When the envelope was sent |
|
||||
| `completedDateTime` | string | When all recipients completed signing |
|
||||
| `createdDateTime` | string | When the envelope was created |
|
||||
| `statusChangedDateTime` | string | When the status last changed |
|
||||
| `voidedReason` | string | Reason the envelope was voided |
|
||||
| `signerCount` | number | Number of signers |
|
||||
| `documentCount` | number | Number of documents |
|
||||
|
||||
### `docusign_list_envelopes`
|
||||
|
||||
List envelopes from your DocuSign account with optional filters
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `fromDate` | string | No | Start date filter \(ISO 8601\). Defaults to 30 days ago |
|
||||
| `toDate` | string | No | End date filter \(ISO 8601\) |
|
||||
| `envelopeStatus` | string | No | Filter by status: created, sent, delivered, completed, declined, voided |
|
||||
| `searchText` | string | No | Search text to filter envelopes |
|
||||
| `count` | string | No | Maximum number of envelopes to return \(default: 25\) |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `envelopes` | array | Array of DocuSign envelopes |
|
||||
| ↳ `envelopeId` | string | Unique envelope identifier |
|
||||
| ↳ `status` | string | Envelope status \(created, sent, delivered, completed, declined, voided\) |
|
||||
| ↳ `emailSubject` | string | Email subject line |
|
||||
| ↳ `sentDateTime` | string | ISO 8601 datetime when envelope was sent |
|
||||
| ↳ `completedDateTime` | string | ISO 8601 datetime when envelope was completed |
|
||||
| ↳ `createdDateTime` | string | ISO 8601 datetime when envelope was created |
|
||||
| ↳ `statusChangedDateTime` | string | ISO 8601 datetime of last status change |
|
||||
| `totalSetSize` | number | Total number of matching envelopes |
|
||||
| `resultSetSize` | number | Number of envelopes returned in this response |
|
||||
|
||||
### `docusign_void_envelope`
|
||||
|
||||
Void (cancel) a sent DocuSign envelope that has not yet been completed
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `envelopeId` | string | Yes | The envelope ID to void |
|
||||
| `voidedReason` | string | Yes | Reason for voiding the envelope |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `envelopeId` | string | Voided envelope ID |
|
||||
| `status` | string | Envelope status \(voided\) |
|
||||
|
||||
### `docusign_download_document`
|
||||
|
||||
Download a signed document from a completed DocuSign envelope
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `envelopeId` | string | Yes | The envelope ID containing the document |
|
||||
| `documentId` | string | No | Specific document ID to download, or "combined" for all documents merged \(default: "combined"\) |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `base64Content` | string | Base64-encoded document content |
|
||||
| `mimeType` | string | MIME type of the document |
|
||||
| `fileName` | string | Original file name |
|
||||
|
||||
### `docusign_list_templates`
|
||||
|
||||
List available templates in your DocuSign account
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `searchText` | string | No | Search text to filter templates by name |
|
||||
| `count` | string | No | Maximum number of templates to return |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `templates` | array | Array of DocuSign templates |
|
||||
| ↳ `templateId` | string | Template identifier |
|
||||
| ↳ `name` | string | Template name |
|
||||
| ↳ `description` | string | Template description |
|
||||
| ↳ `shared` | boolean | Whether template is shared |
|
||||
| ↳ `created` | string | ISO 8601 creation date |
|
||||
| ↳ `lastModified` | string | ISO 8601 last modified date |
|
||||
| `totalSetSize` | number | Total number of matching templates |
|
||||
| `resultSetSize` | number | Number of templates returned in this response |
|
||||
|
||||
### `docusign_list_recipients`
|
||||
|
||||
Get the recipient status details for a DocuSign envelope
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `envelopeId` | string | Yes | The envelope ID to get recipients for |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `signers` | array | Array of DocuSign recipients |
|
||||
| ↳ `recipientId` | string | Recipient identifier |
|
||||
| ↳ `name` | string | Recipient name |
|
||||
| ↳ `email` | string | Recipient email address |
|
||||
| ↳ `status` | string | Recipient signing status \(sent, delivered, completed, declined\) |
|
||||
| ↳ `signedDateTime` | string | ISO 8601 datetime when recipient signed |
|
||||
| ↳ `deliveredDateTime` | string | ISO 8601 datetime when delivered to recipient |
|
||||
| `carbonCopies` | array | Array of carbon copy recipients |
|
||||
| ↳ `recipientId` | string | Recipient ID |
|
||||
| ↳ `name` | string | Recipient name |
|
||||
| ↳ `email` | string | Recipient email |
|
||||
| ↳ `status` | string | Recipient status |
|
||||
|
||||
|
||||
@@ -53,6 +53,9 @@ Extract structured content from web pages with comprehensive metadata support. C
|
||||
| `url` | string | Yes | The URL to scrape content from \(e.g., "https://example.com/page"\) |
|
||||
| `scrapeOptions` | json | No | Options for content scraping |
|
||||
| `apiKey` | string | Yes | Firecrawl API key |
|
||||
| `pricing` | custom | No | No description |
|
||||
| `metadata` | string | No | No description |
|
||||
| `rateLimit` | string | No | No description |
|
||||
|
||||
#### Output
|
||||
|
||||
@@ -86,6 +89,9 @@ Search for information on the web using Firecrawl
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `query` | string | Yes | The search query to use |
|
||||
| `apiKey` | string | Yes | Firecrawl API key |
|
||||
| `pricing` | custom | No | No description |
|
||||
| `metadata` | string | No | No description |
|
||||
| `rateLimit` | string | No | No description |
|
||||
|
||||
#### Output
|
||||
|
||||
@@ -123,6 +129,9 @@ Crawl entire websites and extract structured content from all accessible pages
|
||||
| `includePaths` | json | No | URL paths to include in crawling \(e.g., \["/docs/*", "/api/*"\]\). Only these paths will be crawled |
|
||||
| `onlyMainContent` | boolean | No | Extract only main content from pages |
|
||||
| `apiKey` | string | Yes | Firecrawl API Key |
|
||||
| `pricing` | custom | No | No description |
|
||||
| `metadata` | string | No | No description |
|
||||
| `rateLimit` | string | No | No description |
|
||||
|
||||
#### Output
|
||||
|
||||
@@ -142,7 +151,6 @@ Crawl entire websites and extract structured content from all accessible pages
|
||||
| ↳ `statusCode` | number | HTTP status code |
|
||||
| ↳ `ogLocaleAlternate` | array | Alternate locale versions |
|
||||
| `total` | number | Total number of pages found during crawl |
|
||||
| `creditsUsed` | number | Number of credits consumed by the crawl operation |
|
||||
|
||||
### `firecrawl_map`
|
||||
|
||||
@@ -161,6 +169,9 @@ Get a complete list of URLs from any website quickly and reliably. Useful for di
|
||||
| `timeout` | number | No | Request timeout in milliseconds |
|
||||
| `location` | json | No | Geographic context for proxying \(country, languages\) |
|
||||
| `apiKey` | string | Yes | Firecrawl API key |
|
||||
| `pricing` | custom | No | No description |
|
||||
| `metadata` | string | No | No description |
|
||||
| `rateLimit` | string | No | No description |
|
||||
|
||||
#### Output
|
||||
|
||||
@@ -187,6 +198,9 @@ Extract structured data from entire webpages using natural language prompts and
|
||||
| `ignoreInvalidURLs` | boolean | No | Skip invalid URLs in the array \(default: true\) |
|
||||
| `scrapeOptions` | json | No | Advanced scraping configuration options |
|
||||
| `apiKey` | string | Yes | Firecrawl API key |
|
||||
| `pricing` | custom | No | No description |
|
||||
| `metadata` | string | No | No description |
|
||||
| `rateLimit` | string | No | No description |
|
||||
|
||||
#### Output
|
||||
|
||||
@@ -217,7 +231,6 @@ Autonomous web data extraction agent. Searches and gathers information based on
|
||||
| `success` | boolean | Whether the agent operation was successful |
|
||||
| `status` | string | Current status of the agent job \(processing, completed, failed\) |
|
||||
| `data` | object | Extracted data from the agent |
|
||||
| `creditsUsed` | number | Number of credits consumed by this agent task |
|
||||
| `expiresAt` | string | Timestamp when the results expire \(24 hours\) |
|
||||
| `sources` | object | Array of source URLs used by the agent |
|
||||
|
||||
|
||||
@@ -46,6 +46,8 @@ Search for books using the Google Books API
|
||||
| `startIndex` | number | No | Index of the first result to return \(for pagination\) |
|
||||
| `maxResults` | number | No | Maximum number of results to return \(1-40\) |
|
||||
| `langRestrict` | string | No | Restrict results to a specific language \(ISO 639-1 code\) |
|
||||
| `pricing` | per_request | No | No description |
|
||||
| `rateLimit` | string | No | No description |
|
||||
|
||||
#### Output
|
||||
|
||||
@@ -82,6 +84,8 @@ Get detailed information about a specific book volume
|
||||
| `apiKey` | string | Yes | Google Books API key |
|
||||
| `volumeId` | string | Yes | The ID of the volume to retrieve |
|
||||
| `projection` | string | No | Projection level \(full, lite\) |
|
||||
| `pricing` | per_request | No | No description |
|
||||
| `rateLimit` | string | No | No description |
|
||||
|
||||
#### Output
|
||||
|
||||
|
||||
@@ -50,6 +50,8 @@ Get current air quality data for a location
|
||||
| `lat` | number | Yes | Latitude coordinate |
|
||||
| `lng` | number | Yes | Longitude coordinate |
|
||||
| `languageCode` | string | No | Language code for the response \(e.g., "en", "es"\) |
|
||||
| `pricing` | per_request | No | No description |
|
||||
| `rateLimit` | string | No | No description |
|
||||
|
||||
#### Output
|
||||
|
||||
@@ -91,6 +93,8 @@ Get directions and route information between two locations
|
||||
| `waypoints` | json | No | Array of intermediate waypoints |
|
||||
| `units` | string | No | Unit system: metric or imperial |
|
||||
| `language` | string | No | Language code for results \(e.g., en, es, fr\) |
|
||||
| `pricing` | per_request | No | No description |
|
||||
| `rateLimit` | string | No | No description |
|
||||
|
||||
#### Output
|
||||
|
||||
@@ -135,6 +139,8 @@ Calculate travel distance and time between multiple origins and destinations
|
||||
| `avoid` | string | No | Features to avoid: tolls, highways, or ferries |
|
||||
| `units` | string | No | Unit system: metric or imperial |
|
||||
| `language` | string | No | Language code for results \(e.g., en, es, fr\) |
|
||||
| `pricing` | per_request | No | No description |
|
||||
| `rateLimit` | string | No | No description |
|
||||
|
||||
#### Output
|
||||
|
||||
@@ -163,6 +169,8 @@ Get elevation data for a location
|
||||
| `apiKey` | string | Yes | Google Maps API key |
|
||||
| `lat` | number | Yes | Latitude coordinate |
|
||||
| `lng` | number | Yes | Longitude coordinate |
|
||||
| `pricing` | per_request | No | No description |
|
||||
| `rateLimit` | string | No | No description |
|
||||
|
||||
#### Output
|
||||
|
||||
@@ -185,6 +193,8 @@ Convert an address into geographic coordinates (latitude and longitude)
|
||||
| `address` | string | Yes | The address to geocode |
|
||||
| `language` | string | No | Language code for results \(e.g., en, es, fr\) |
|
||||
| `region` | string | No | Region bias as a ccTLD code \(e.g., us, uk\) |
|
||||
| `pricing` | per_request | No | No description |
|
||||
| `rateLimit` | string | No | No description |
|
||||
|
||||
#### Output
|
||||
|
||||
@@ -217,6 +227,8 @@ Geolocate a device using WiFi access points, cell towers, or IP address
|
||||
| `considerIp` | boolean | No | Whether to use IP address for geolocation \(default: true\) |
|
||||
| `cellTowers` | array | No | Array of cell tower objects with cellId, locationAreaCode, mobileCountryCode, mobileNetworkCode |
|
||||
| `wifiAccessPoints` | array | No | Array of WiFi access point objects with macAddress \(required\), signalStrength, etc. |
|
||||
| `pricing` | per_request | No | No description |
|
||||
| `rateLimit` | string | No | No description |
|
||||
|
||||
#### Output
|
||||
|
||||
@@ -238,6 +250,8 @@ Get detailed information about a specific place
|
||||
| `placeId` | string | Yes | Google Place ID |
|
||||
| `fields` | string | No | Comma-separated list of fields to return |
|
||||
| `language` | string | No | Language code for results \(e.g., en, es, fr\) |
|
||||
| `pricing` | per_request | No | No description |
|
||||
| `rateLimit` | string | No | No description |
|
||||
|
||||
#### Output
|
||||
|
||||
@@ -290,6 +304,8 @@ Search for places using a text query
|
||||
| `type` | string | No | Place type filter \(e.g., restaurant, cafe, hotel\) |
|
||||
| `language` | string | No | Language code for results \(e.g., en, es, fr\) |
|
||||
| `region` | string | No | Region bias as a ccTLD code \(e.g., us, uk\) |
|
||||
| `pricing` | per_request | No | No description |
|
||||
| `rateLimit` | string | No | No description |
|
||||
|
||||
#### Output
|
||||
|
||||
@@ -322,6 +338,8 @@ Convert geographic coordinates (latitude and longitude) into a human-readable ad
|
||||
| `lat` | number | Yes | Latitude coordinate |
|
||||
| `lng` | number | Yes | Longitude coordinate |
|
||||
| `language` | string | No | Language code for results \(e.g., en, es, fr\) |
|
||||
| `pricing` | per_request | No | No description |
|
||||
| `rateLimit` | string | No | No description |
|
||||
|
||||
#### Output
|
||||
|
||||
@@ -346,6 +364,8 @@ Snap GPS coordinates to the nearest road segment
|
||||
| `apiKey` | string | Yes | Google Maps API key with Roads API enabled |
|
||||
| `path` | string | Yes | Pipe-separated list of lat,lng coordinates \(e.g., "60.170880,24.942795\|60.170879,24.942796"\) |
|
||||
| `interpolate` | boolean | No | Whether to interpolate additional points along the road |
|
||||
| `pricing` | per_request | No | No description |
|
||||
| `rateLimit` | string | No | No description |
|
||||
|
||||
#### Output
|
||||
|
||||
@@ -399,6 +419,8 @@ Get timezone information for a location
|
||||
| `lng` | number | Yes | Longitude coordinate |
|
||||
| `timestamp` | number | No | Unix timestamp to determine DST offset \(defaults to current time\) |
|
||||
| `language` | string | No | Language code for timezone name \(e.g., en, es, fr\) |
|
||||
| `pricing` | per_request | No | No description |
|
||||
| `rateLimit` | string | No | No description |
|
||||
|
||||
#### Output
|
||||
|
||||
@@ -424,6 +446,8 @@ Validate and standardize a postal address
|
||||
| `regionCode` | string | No | ISO 3166-1 alpha-2 country code \(e.g., "US", "CA"\) |
|
||||
| `locality` | string | No | City or locality name |
|
||||
| `enableUspsCass` | boolean | No | Enable USPS CASS validation for US addresses |
|
||||
| `pricing` | per_request | No | No description |
|
||||
| `rateLimit` | string | No | No description |
|
||||
|
||||
#### Output
|
||||
|
||||
|
||||
@@ -55,6 +55,8 @@ Analyze a webpage for performance, accessibility, SEO, and best practices using
|
||||
| `category` | string | No | Lighthouse categories to analyze \(comma-separated\): performance, accessibility, best-practices, seo |
|
||||
| `strategy` | string | No | Analysis strategy: desktop or mobile |
|
||||
| `locale` | string | No | Locale for results \(e.g., en, fr, de\) |
|
||||
| `pricing` | per_request | No | No description |
|
||||
| `rateLimit` | string | No | No description |
|
||||
|
||||
#### Output
|
||||
|
||||
|
||||
@@ -43,6 +43,9 @@ Translate text between languages using the Google Cloud Translation API. Support
|
||||
| `target` | string | Yes | Target language code \(e.g., "es", "fr", "de", "ja"\) |
|
||||
| `source` | string | No | Source language code. If omitted, the API will auto-detect the source language. |
|
||||
| `format` | string | No | Format of the text: "text" for plain text, "html" for HTML content |
|
||||
| `pricing` | custom | No | No description |
|
||||
| `metadata` | string | No | No description |
|
||||
| `rateLimit` | string | No | No description |
|
||||
|
||||
#### Output
|
||||
|
||||
@@ -61,6 +64,9 @@ Detect the language of text using the Google Cloud Translation API.
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | Google Cloud API key with Cloud Translation API enabled |
|
||||
| `text` | string | Yes | The text to detect the language of |
|
||||
| `pricing` | custom | No | No description |
|
||||
| `metadata` | string | No | No description |
|
||||
| `rateLimit` | string | No | No description |
|
||||
|
||||
#### Output
|
||||
|
||||
|
||||
@@ -138,6 +138,26 @@ Get the full transcript of a recording
|
||||
| ↳ `end` | number | End timestamp in ms |
|
||||
| ↳ `text` | string | Transcript text |
|
||||
|
||||
### `grain_list_views`
|
||||
|
||||
List available Grain views for webhook subscriptions
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | Grain API key \(Personal Access Token\) |
|
||||
| `typeFilter` | string | No | Optional view type filter: recordings, highlights, or stories |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `views` | array | Array of Grain views |
|
||||
| ↳ `id` | string | View UUID |
|
||||
| ↳ `name` | string | View name |
|
||||
| ↳ `type` | string | View type: recordings, highlights, or stories |
|
||||
|
||||
### `grain_list_teams`
|
||||
|
||||
List all teams in the workspace
|
||||
@@ -185,15 +205,9 @@ Create a webhook to receive recording events
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | Grain API key \(Personal Access Token\) |
|
||||
| `hookUrl` | string | Yes | Webhook endpoint URL \(e.g., "https://example.com/webhooks/grain"\) |
|
||||
| `hookType` | string | Yes | Type of webhook: "recording_added" or "upload_status" |
|
||||
| `filterBeforeDatetime` | string | No | Filter: recordings before this ISO8601 date \(e.g., "2024-01-15T00:00:00Z"\) |
|
||||
| `filterAfterDatetime` | string | No | Filter: recordings after this ISO8601 date \(e.g., "2024-01-01T00:00:00Z"\) |
|
||||
| `filterParticipantScope` | string | No | Filter: "internal" or "external" |
|
||||
| `filterTeamId` | string | No | Filter: specific team UUID \(e.g., "a1b2c3d4-e5f6-7890-abcd-ef1234567890"\) |
|
||||
| `filterMeetingTypeId` | string | No | Filter: specific meeting type UUID \(e.g., "a1b2c3d4-e5f6-7890-abcd-ef1234567890"\) |
|
||||
| `includeHighlights` | boolean | No | Include highlights in webhook payload |
|
||||
| `includeParticipants` | boolean | No | Include participants in webhook payload |
|
||||
| `includeAiSummary` | boolean | No | Include AI summary in webhook payload |
|
||||
| `viewId` | string | Yes | Grain view ID from GET /_/public-api/views |
|
||||
| `actions` | array | No | Optional list of actions to subscribe to: added, updated, removed |
|
||||
| `items` | string | No | No description |
|
||||
|
||||
#### Output
|
||||
|
||||
@@ -202,9 +216,8 @@ Create a webhook to receive recording events
|
||||
| `id` | string | Hook UUID |
|
||||
| `enabled` | boolean | Whether hook is active |
|
||||
| `hook_url` | string | The webhook URL |
|
||||
| `hook_type` | string | Type of hook: recording_added or upload_status |
|
||||
| `filter` | object | Applied filters |
|
||||
| `include` | object | Included fields |
|
||||
| `view_id` | string | Grain view ID for the webhook |
|
||||
| `actions` | array | Configured actions for the webhook |
|
||||
| `inserted_at` | string | ISO8601 creation timestamp |
|
||||
|
||||
### `grain_list_hooks`
|
||||
@@ -225,9 +238,8 @@ List all webhooks for the account
|
||||
| ↳ `id` | string | Hook UUID |
|
||||
| ↳ `enabled` | boolean | Whether hook is active |
|
||||
| ↳ `hook_url` | string | Webhook URL |
|
||||
| ↳ `hook_type` | string | Type: recording_added or upload_status |
|
||||
| ↳ `filter` | object | Applied filters |
|
||||
| ↳ `include` | object | Included fields |
|
||||
| ↳ `view_id` | string | Grain view ID |
|
||||
| ↳ `actions` | array | Configured actions |
|
||||
| ↳ `inserted_at` | string | Creation timestamp |
|
||||
|
||||
### `grain_delete_hook`
|
||||
|
||||
@@ -64,6 +64,7 @@ Extract and process web content into clean, LLM-friendly text using Jina AI Read
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `content` | string | The extracted content from the URL, processed into clean, LLM-friendly text |
|
||||
| `tokensUsed` | number | Number of Jina tokens consumed by this request |
|
||||
|
||||
### `jina_search`
|
||||
|
||||
@@ -97,5 +98,6 @@ Search the web and return top 5 results with LLM-friendly content. Each result i
|
||||
| ↳ `content` | string | LLM-friendly extracted content |
|
||||
| ↳ `usage` | object | Token usage information |
|
||||
| ↳ `tokens` | number | Number of tokens consumed by this request |
|
||||
| `tokensUsed` | number | Number of Jina tokens consumed by this request |
|
||||
|
||||
|
||||
|
||||
@@ -122,6 +122,37 @@ Create a new document in a knowledge base
|
||||
| `message` | string | Success or error message describing the operation result |
|
||||
| `documentId` | string | ID of the created document |
|
||||
|
||||
### `knowledge_upsert_document`
|
||||
|
||||
Create or update a document in a knowledge base. If a document with the given ID or filename already exists, it will be replaced with the new content.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `knowledgeBaseId` | string | Yes | ID of the knowledge base containing the document |
|
||||
| `documentId` | string | No | Optional ID of an existing document to update. If not provided, lookup is done by filename. |
|
||||
| `name` | string | Yes | Name of the document |
|
||||
| `content` | string | Yes | Content of the document |
|
||||
| `documentTags` | json | No | Document tags |
|
||||
| `documentTags` | string | No | No description |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `data` | object | Information about the upserted document |
|
||||
| ↳ `documentId` | string | Document ID |
|
||||
| ↳ `documentName` | string | Document name |
|
||||
| ↳ `type` | string | Document type |
|
||||
| ↳ `enabled` | boolean | Whether the document is enabled |
|
||||
| ↳ `isUpdate` | boolean | Whether an existing document was replaced |
|
||||
| ↳ `previousDocumentId` | string | ID of the document that was replaced, if any |
|
||||
| ↳ `createdAt` | string | Creation timestamp |
|
||||
| ↳ `updatedAt` | string | Last update timestamp |
|
||||
| `message` | string | Success or error message describing the operation result |
|
||||
| `documentId` | string | ID of the upserted document |
|
||||
|
||||
### `knowledge_list_tags`
|
||||
|
||||
List all tag definitions for a knowledge base
|
||||
|
||||
@@ -51,6 +51,9 @@ Search the web for information using Linkup
|
||||
| `includeDomains` | string | No | Comma-separated list of domain names to restrict search results to |
|
||||
| `includeInlineCitations` | boolean | No | Add inline citations to answers \(only applies when outputType is "sourcedAnswer"\) |
|
||||
| `includeSources` | boolean | No | Include sources in response |
|
||||
| `pricing` | custom | No | No description |
|
||||
| `metadata` | string | No | No description |
|
||||
| `rateLimit` | string | No | No description |
|
||||
|
||||
#### Output
|
||||
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
"asana",
|
||||
"ashby",
|
||||
"attio",
|
||||
"box",
|
||||
"brandfetch",
|
||||
"browser_use",
|
||||
"calcom",
|
||||
@@ -27,6 +28,7 @@
|
||||
"datadog",
|
||||
"devin",
|
||||
"discord",
|
||||
"docusign",
|
||||
"dropbox",
|
||||
"dspy",
|
||||
"dub",
|
||||
|
||||
@@ -49,6 +49,9 @@ Generate completions using Perplexity AI chat models
|
||||
| `max_tokens` | number | No | Maximum number of tokens to generate \(e.g., 1024, 2048, 4096\) |
|
||||
| `temperature` | number | No | Sampling temperature between 0 and 1 \(e.g., 0.0 for deterministic, 0.7 for creative\) |
|
||||
| `apiKey` | string | Yes | Perplexity API key |
|
||||
| `pricing` | custom | No | No description |
|
||||
| `metadata` | string | No | No description |
|
||||
| `rateLimit` | string | No | No description |
|
||||
|
||||
#### Output
|
||||
|
||||
@@ -78,6 +81,8 @@ Get ranked search results from Perplexity
|
||||
| `search_after_date` | string | No | Include only content published after this date \(format: MM/DD/YYYY\) |
|
||||
| `search_before_date` | string | No | Include only content published before this date \(format: MM/DD/YYYY\) |
|
||||
| `apiKey` | string | Yes | Perplexity API key |
|
||||
| `pricing` | per_request | No | No description |
|
||||
| `rateLimit` | string | No | No description |
|
||||
|
||||
#### Output
|
||||
|
||||
|
||||
@@ -47,6 +47,9 @@ A powerful web search tool that provides access to Google search results through
|
||||
| `hl` | string | No | Language code for search results \(e.g., "en", "es", "de", "fr"\) |
|
||||
| `type` | string | No | Type of search to perform \(e.g., "search", "news", "images", "videos", "places", "shopping"\) |
|
||||
| `apiKey` | string | Yes | Serper API Key |
|
||||
| `pricing` | custom | No | No description |
|
||||
| `metadata` | string | No | No description |
|
||||
| `rateLimit` | string | No | No description |
|
||||
|
||||
#### Output
|
||||
|
||||
|
||||
@@ -142,7 +142,7 @@ export async function POST(request: NextRequest) {
|
||||
quantity: currentQuantity,
|
||||
},
|
||||
],
|
||||
proration_behavior: 'create_prorations',
|
||||
proration_behavior: 'always_invoice',
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -102,7 +102,7 @@ async function handleLocalFile(filename: string, userId: string): Promise<NextRe
|
||||
throw new FileNotFoundError(`File not found: ${filename}`)
|
||||
}
|
||||
|
||||
const filePath = findLocalFile(filename)
|
||||
const filePath = await findLocalFile(filename)
|
||||
|
||||
if (!filePath) {
|
||||
throw new FileNotFoundError(`File not found: ${filename}`)
|
||||
@@ -228,7 +228,7 @@ async function handleCloudProxyPublic(
|
||||
|
||||
async function handleLocalFilePublic(filename: string): Promise<NextResponse> {
|
||||
try {
|
||||
const filePath = findLocalFile(filename)
|
||||
const filePath = await findLocalFile(filename)
|
||||
|
||||
if (!filePath) {
|
||||
throw new FileNotFoundError(`File not found: ${filename}`)
|
||||
|
||||
@@ -75,7 +75,7 @@ export async function POST(request: NextRequest) {
|
||||
const uploadResults = []
|
||||
|
||||
for (const file of files) {
|
||||
const originalName = file.name || 'untitled'
|
||||
const originalName = file.name || 'untitled.md'
|
||||
|
||||
if (!validateFileExtension(originalName)) {
|
||||
const extension = originalName.split('.').pop()?.toLowerCase() || 'unknown'
|
||||
|
||||
@@ -331,7 +331,7 @@ describe('extractFilename', () => {
|
||||
|
||||
describe('findLocalFile - Path Traversal Security Tests', () => {
|
||||
describe('path traversal attack prevention', () => {
|
||||
it.concurrent('should reject classic path traversal attacks', () => {
|
||||
it.concurrent('should reject classic path traversal attacks', async () => {
|
||||
const maliciousInputs = [
|
||||
'../../../etc/passwd',
|
||||
'..\\..\\..\\windows\\system32\\config\\sam',
|
||||
@@ -340,35 +340,35 @@ describe('findLocalFile - Path Traversal Security Tests', () => {
|
||||
'..\\config.ini',
|
||||
]
|
||||
|
||||
maliciousInputs.forEach((input) => {
|
||||
const result = findLocalFile(input)
|
||||
for (const input of maliciousInputs) {
|
||||
const result = await findLocalFile(input)
|
||||
expect(result).toBeNull()
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
it.concurrent('should reject encoded path traversal attempts', () => {
|
||||
it.concurrent('should reject encoded path traversal attempts', async () => {
|
||||
const encodedInputs = [
|
||||
'%2e%2e%2f%2e%2e%2f%65%74%63%2f%70%61%73%73%77%64', // ../../../etc/passwd
|
||||
'..%2f..%2fetc%2fpasswd',
|
||||
'..%5c..%5cconfig.ini',
|
||||
]
|
||||
|
||||
encodedInputs.forEach((input) => {
|
||||
const result = findLocalFile(input)
|
||||
for (const input of encodedInputs) {
|
||||
const result = await findLocalFile(input)
|
||||
expect(result).toBeNull()
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
it.concurrent('should reject mixed path separators', () => {
|
||||
it.concurrent('should reject mixed path separators', async () => {
|
||||
const mixedInputs = ['../..\\config.txt', '..\\../secret.ini', '/..\\..\\system32']
|
||||
|
||||
mixedInputs.forEach((input) => {
|
||||
const result = findLocalFile(input)
|
||||
for (const input of mixedInputs) {
|
||||
const result = await findLocalFile(input)
|
||||
expect(result).toBeNull()
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
it.concurrent('should reject filenames with dangerous characters', () => {
|
||||
it.concurrent('should reject filenames with dangerous characters', async () => {
|
||||
const dangerousInputs = [
|
||||
'file:with:colons.txt',
|
||||
'file|with|pipes.txt',
|
||||
@@ -376,43 +376,45 @@ describe('findLocalFile - Path Traversal Security Tests', () => {
|
||||
'file*with*asterisks.txt',
|
||||
]
|
||||
|
||||
dangerousInputs.forEach((input) => {
|
||||
const result = findLocalFile(input)
|
||||
for (const input of dangerousInputs) {
|
||||
const result = await findLocalFile(input)
|
||||
expect(result).toBeNull()
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
it.concurrent('should reject null and empty inputs', () => {
|
||||
expect(findLocalFile('')).toBeNull()
|
||||
expect(findLocalFile(' ')).toBeNull()
|
||||
expect(findLocalFile('\t\n')).toBeNull()
|
||||
it.concurrent('should reject null and empty inputs', async () => {
|
||||
expect(await findLocalFile('')).toBeNull()
|
||||
expect(await findLocalFile(' ')).toBeNull()
|
||||
expect(await findLocalFile('\t\n')).toBeNull()
|
||||
})
|
||||
|
||||
it.concurrent('should reject filenames that become empty after sanitization', () => {
|
||||
it.concurrent('should reject filenames that become empty after sanitization', async () => {
|
||||
const emptyAfterSanitization = ['../..', '..\\..\\', '////', '....', '..']
|
||||
|
||||
emptyAfterSanitization.forEach((input) => {
|
||||
const result = findLocalFile(input)
|
||||
for (const input of emptyAfterSanitization) {
|
||||
const result = await findLocalFile(input)
|
||||
expect(result).toBeNull()
|
||||
})
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe('security validation passes for legitimate files', () => {
|
||||
it.concurrent('should accept properly formatted filenames without throwing errors', () => {
|
||||
const legitimateInputs = [
|
||||
'document.pdf',
|
||||
'image.png',
|
||||
'data.csv',
|
||||
'report-2024.doc',
|
||||
'file_with_underscores.txt',
|
||||
'file-with-dashes.json',
|
||||
]
|
||||
it.concurrent(
|
||||
'should accept properly formatted filenames without throwing errors',
|
||||
async () => {
|
||||
const legitimateInputs = [
|
||||
'document.pdf',
|
||||
'image.png',
|
||||
'data.csv',
|
||||
'report-2024.doc',
|
||||
'file_with_underscores.txt',
|
||||
'file-with-dashes.json',
|
||||
]
|
||||
|
||||
legitimateInputs.forEach((input) => {
|
||||
// Should not throw security errors for legitimate filenames
|
||||
expect(() => findLocalFile(input)).not.toThrow()
|
||||
})
|
||||
})
|
||||
for (const input of legitimateInputs) {
|
||||
await expect(findLocalFile(input)).resolves.toBeDefined()
|
||||
}
|
||||
}
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,8 +1,5 @@
|
||||
import { existsSync } from 'fs'
|
||||
import path from 'path'
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { NextResponse } from 'next/server'
|
||||
import { UPLOAD_DIR } from '@/lib/uploads/config'
|
||||
import { sanitizeFileKey } from '@/lib/uploads/utils/file-utils'
|
||||
|
||||
const logger = createLogger('FilesUtils')
|
||||
@@ -123,76 +120,29 @@ export function extractFilename(path: string): string {
|
||||
return filename
|
||||
}
|
||||
|
||||
function sanitizeFilename(filename: string): string {
|
||||
if (!filename || typeof filename !== 'string') {
|
||||
throw new Error('Invalid filename provided')
|
||||
}
|
||||
|
||||
if (!filename.includes('/')) {
|
||||
throw new Error('File key must include a context prefix (e.g., kb/, workspace/, execution/)')
|
||||
}
|
||||
|
||||
const segments = filename.split('/')
|
||||
|
||||
const sanitizedSegments = segments.map((segment) => {
|
||||
if (segment === '..' || segment === '.') {
|
||||
throw new Error('Path traversal detected')
|
||||
}
|
||||
|
||||
const sanitized = segment.replace(/\.\./g, '').replace(/[\\]/g, '').replace(/^\./g, '').trim()
|
||||
|
||||
if (!sanitized) {
|
||||
throw new Error('Invalid or empty path segment after sanitization')
|
||||
}
|
||||
|
||||
if (
|
||||
sanitized.includes(':') ||
|
||||
sanitized.includes('|') ||
|
||||
sanitized.includes('?') ||
|
||||
sanitized.includes('*') ||
|
||||
sanitized.includes('\x00') ||
|
||||
/[\x00-\x1F\x7F]/.test(sanitized)
|
||||
) {
|
||||
throw new Error('Path segment contains invalid characters')
|
||||
}
|
||||
|
||||
return sanitized
|
||||
})
|
||||
|
||||
return sanitizedSegments.join(path.sep)
|
||||
}
|
||||
|
||||
export function findLocalFile(filename: string): string | null {
|
||||
export async function findLocalFile(filename: string): Promise<string | null> {
|
||||
try {
|
||||
const sanitizedFilename = sanitizeFileKey(filename)
|
||||
|
||||
// Reject if sanitized filename is empty or only contains path separators/dots
|
||||
if (!sanitizedFilename || !sanitizedFilename.trim() || /^[/\\.\s]+$/.test(sanitizedFilename)) {
|
||||
return null
|
||||
}
|
||||
|
||||
const possiblePaths = [
|
||||
path.join(UPLOAD_DIR, sanitizedFilename),
|
||||
path.join(process.cwd(), 'uploads', sanitizedFilename),
|
||||
]
|
||||
const { existsSync } = await import('fs')
|
||||
const path = await import('path')
|
||||
const { UPLOAD_DIR_SERVER } = await import('@/lib/uploads/core/setup.server')
|
||||
|
||||
for (const filePath of possiblePaths) {
|
||||
const resolvedPath = path.resolve(filePath)
|
||||
const allowedDirs = [path.resolve(UPLOAD_DIR), path.resolve(process.cwd(), 'uploads')]
|
||||
const resolvedPath = path.join(UPLOAD_DIR_SERVER, sanitizedFilename)
|
||||
|
||||
// Must be within allowed directory but NOT the directory itself
|
||||
const isWithinAllowedDir = allowedDirs.some(
|
||||
(allowedDir) =>
|
||||
resolvedPath.startsWith(allowedDir + path.sep) && resolvedPath !== allowedDir
|
||||
)
|
||||
if (
|
||||
!resolvedPath.startsWith(UPLOAD_DIR_SERVER + path.sep) ||
|
||||
resolvedPath === UPLOAD_DIR_SERVER
|
||||
) {
|
||||
return null
|
||||
}
|
||||
|
||||
if (!isWithinAllowedDir) {
|
||||
continue
|
||||
}
|
||||
|
||||
if (existsSync(resolvedPath)) {
|
||||
return resolvedPath
|
||||
}
|
||||
if (existsSync(resolvedPath)) {
|
||||
return resolvedPath
|
||||
}
|
||||
|
||||
return null
|
||||
|
||||
@@ -161,7 +161,7 @@ export async function PUT(request: NextRequest, { params }: { params: Promise<{
|
||||
quantity: newSeatCount,
|
||||
},
|
||||
],
|
||||
proration_behavior: 'create_prorations', // Stripe's default - charge/credit immediately
|
||||
proration_behavior: 'always_invoice',
|
||||
}
|
||||
)
|
||||
|
||||
@@ -213,7 +213,7 @@ export async function PUT(request: NextRequest, { params }: { params: Promise<{
|
||||
oldSeats: currentSeats,
|
||||
newSeats: newSeatCount,
|
||||
updatedBy: session.user.id,
|
||||
prorationBehavior: 'create_prorations',
|
||||
prorationBehavior: 'always_invoice',
|
||||
})
|
||||
|
||||
return NextResponse.json({
|
||||
|
||||
140
apps/sim/app/api/tools/box/upload/route.ts
Normal file
140
apps/sim/app/api/tools/box/upload/route.ts
Normal file
@@ -0,0 +1,140 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { z } from 'zod'
|
||||
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
||||
import { generateRequestId } from '@/lib/core/utils/request'
|
||||
import { FileInputSchema } from '@/lib/uploads/utils/file-schemas'
|
||||
import { processFilesToUserFiles, type RawFileInput } from '@/lib/uploads/utils/file-utils'
|
||||
import { downloadFileFromStorage } from '@/lib/uploads/utils/file-utils.server'
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
const logger = createLogger('BoxUploadAPI')
|
||||
|
||||
const BoxUploadSchema = z.object({
|
||||
accessToken: z.string().min(1, 'Access token is required'),
|
||||
parentFolderId: z.string().min(1, 'Parent folder ID is required'),
|
||||
file: FileInputSchema.optional().nullable(),
|
||||
fileContent: z.string().optional().nullable(),
|
||||
fileName: z.string().optional().nullable(),
|
||||
})
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
const requestId = generateRequestId()
|
||||
|
||||
try {
|
||||
const authResult = await checkInternalAuth(request, { requireWorkflowId: false })
|
||||
|
||||
if (!authResult.success) {
|
||||
logger.warn(`[${requestId}] Unauthorized Box upload attempt: ${authResult.error}`)
|
||||
return NextResponse.json(
|
||||
{ success: false, error: authResult.error || 'Authentication required' },
|
||||
{ status: 401 }
|
||||
)
|
||||
}
|
||||
|
||||
logger.info(`[${requestId}] Authenticated Box upload request via ${authResult.authType}`)
|
||||
|
||||
const body = await request.json()
|
||||
const validatedData = BoxUploadSchema.parse(body)
|
||||
|
||||
let fileBuffer: Buffer
|
||||
let fileName: string
|
||||
|
||||
if (validatedData.file) {
|
||||
const userFiles = processFilesToUserFiles(
|
||||
[validatedData.file as RawFileInput],
|
||||
requestId,
|
||||
logger
|
||||
)
|
||||
|
||||
if (userFiles.length === 0) {
|
||||
return NextResponse.json({ success: false, error: 'Invalid file input' }, { status: 400 })
|
||||
}
|
||||
|
||||
const userFile = userFiles[0]
|
||||
logger.info(`[${requestId}] Downloading file: ${userFile.name} (${userFile.size} bytes)`)
|
||||
|
||||
fileBuffer = await downloadFileFromStorage(userFile, requestId, logger)
|
||||
fileName = validatedData.fileName || userFile.name
|
||||
} else if (validatedData.fileContent) {
|
||||
logger.info(`[${requestId}] Using legacy base64 content input`)
|
||||
fileBuffer = Buffer.from(validatedData.fileContent, 'base64')
|
||||
fileName = validatedData.fileName || 'file'
|
||||
} else {
|
||||
return NextResponse.json({ success: false, error: 'File is required' }, { status: 400 })
|
||||
}
|
||||
|
||||
logger.info(
|
||||
`[${requestId}] Uploading to Box folder ${validatedData.parentFolderId}: ${fileName} (${fileBuffer.length} bytes)`
|
||||
)
|
||||
|
||||
const attributes = JSON.stringify({
|
||||
name: fileName,
|
||||
parent: { id: validatedData.parentFolderId },
|
||||
})
|
||||
|
||||
const formData = new FormData()
|
||||
formData.append('attributes', attributes)
|
||||
formData.append(
|
||||
'file',
|
||||
new Blob([new Uint8Array(fileBuffer)], { type: 'application/octet-stream' }),
|
||||
fileName
|
||||
)
|
||||
|
||||
const response = await fetch('https://upload.box.com/api/2.0/files/content', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Authorization: `Bearer ${validatedData.accessToken}`,
|
||||
},
|
||||
body: formData,
|
||||
})
|
||||
|
||||
const data = await response.json()
|
||||
|
||||
if (!response.ok) {
|
||||
const errorMessage = data.message || 'Failed to upload file'
|
||||
logger.error(`[${requestId}] Box API error:`, { status: response.status, data })
|
||||
return NextResponse.json({ success: false, error: errorMessage }, { status: response.status })
|
||||
}
|
||||
|
||||
const file = data.entries?.[0]
|
||||
|
||||
if (!file) {
|
||||
return NextResponse.json(
|
||||
{ success: false, error: 'No file returned in upload response' },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
|
||||
logger.info(`[${requestId}] File uploaded successfully: ${file.name} (ID: ${file.id})`)
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
output: {
|
||||
id: file.id ?? '',
|
||||
name: file.name ?? '',
|
||||
size: file.size ?? 0,
|
||||
sha1: file.sha1 ?? null,
|
||||
createdAt: file.created_at ?? null,
|
||||
modifiedAt: file.modified_at ?? null,
|
||||
parentId: file.parent?.id ?? null,
|
||||
parentName: file.parent?.name ?? null,
|
||||
},
|
||||
})
|
||||
} catch (error) {
|
||||
if (error instanceof z.ZodError) {
|
||||
logger.warn(`[${requestId}] Validation error:`, error.errors)
|
||||
return NextResponse.json(
|
||||
{ success: false, error: error.errors[0]?.message || 'Validation failed' },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
logger.error(`[${requestId}] Unexpected error:`, error)
|
||||
return NextResponse.json(
|
||||
{ success: false, error: error instanceof Error ? error.message : 'Unknown error' },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
466
apps/sim/app/api/tools/docusign/route.ts
Normal file
466
apps/sim/app/api/tools/docusign/route.ts
Normal file
@@ -0,0 +1,466 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
||||
import { FileInputSchema } from '@/lib/uploads/utils/file-schemas'
|
||||
import { processFilesToUserFiles, type RawFileInput } from '@/lib/uploads/utils/file-utils'
|
||||
import { downloadFileFromStorage } from '@/lib/uploads/utils/file-utils.server'
|
||||
|
||||
const logger = createLogger('DocuSignAPI')
|
||||
|
||||
interface DocuSignAccountInfo {
|
||||
accountId: string
|
||||
baseUri: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves the user's DocuSign account info from their access token
|
||||
* by calling the DocuSign userinfo endpoint.
|
||||
*/
|
||||
async function resolveAccount(accessToken: string): Promise<DocuSignAccountInfo> {
|
||||
const response = await fetch('https://account-d.docusign.com/oauth/userinfo', {
|
||||
headers: { Authorization: `Bearer ${accessToken}` },
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text()
|
||||
logger.error('Failed to resolve DocuSign account', {
|
||||
status: response.status,
|
||||
error: errorText,
|
||||
})
|
||||
throw new Error(`Failed to resolve DocuSign account: ${response.status}`)
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
const accounts = data.accounts ?? []
|
||||
|
||||
const defaultAccount = accounts.find((a: { is_default: boolean }) => a.is_default) ?? accounts[0]
|
||||
if (!defaultAccount) {
|
||||
throw new Error('No DocuSign accounts found for this user')
|
||||
}
|
||||
|
||||
const baseUri = defaultAccount.base_uri
|
||||
if (!baseUri) {
|
||||
throw new Error('DocuSign account is missing base_uri')
|
||||
}
|
||||
|
||||
return {
|
||||
accountId: defaultAccount.account_id,
|
||||
baseUri,
|
||||
}
|
||||
}
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
const authResult = await checkInternalAuth(request, { requireWorkflowId: false })
|
||||
if (!authResult.success) {
|
||||
return NextResponse.json({ success: false, error: 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
const body = await request.json()
|
||||
const { accessToken, operation, ...params } = body
|
||||
|
||||
if (!accessToken) {
|
||||
return NextResponse.json({ success: false, error: 'Access token is required' }, { status: 400 })
|
||||
}
|
||||
|
||||
if (!operation) {
|
||||
return NextResponse.json({ success: false, error: 'Operation is required' }, { status: 400 })
|
||||
}
|
||||
|
||||
try {
|
||||
const account = await resolveAccount(accessToken)
|
||||
const apiBase = `${account.baseUri}/restapi/v2.1/accounts/${account.accountId}`
|
||||
const headers: Record<string, string> = {
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
'Content-Type': 'application/json',
|
||||
}
|
||||
|
||||
switch (operation) {
|
||||
case 'send_envelope':
|
||||
return await handleSendEnvelope(apiBase, headers, params)
|
||||
case 'create_from_template':
|
||||
return await handleCreateFromTemplate(apiBase, headers, params)
|
||||
case 'get_envelope':
|
||||
return await handleGetEnvelope(apiBase, headers, params)
|
||||
case 'list_envelopes':
|
||||
return await handleListEnvelopes(apiBase, headers, params)
|
||||
case 'void_envelope':
|
||||
return await handleVoidEnvelope(apiBase, headers, params)
|
||||
case 'download_document':
|
||||
return await handleDownloadDocument(apiBase, headers, params)
|
||||
case 'list_templates':
|
||||
return await handleListTemplates(apiBase, headers, params)
|
||||
case 'list_recipients':
|
||||
return await handleListRecipients(apiBase, headers, params)
|
||||
default:
|
||||
return NextResponse.json(
|
||||
{ success: false, error: `Unknown operation: ${operation}` },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error('DocuSign API error', { operation, error })
|
||||
const message = error instanceof Error ? error.message : 'Internal server error'
|
||||
return NextResponse.json({ success: false, error: message }, { status: 500 })
|
||||
}
|
||||
}
|
||||
|
||||
async function handleSendEnvelope(
|
||||
apiBase: string,
|
||||
headers: Record<string, string>,
|
||||
params: Record<string, unknown>
|
||||
) {
|
||||
const { signerEmail, signerName, emailSubject, emailBody, ccEmail, ccName, file, status } = params
|
||||
|
||||
if (!signerEmail || !signerName || !emailSubject) {
|
||||
return NextResponse.json(
|
||||
{ success: false, error: 'signerEmail, signerName, and emailSubject are required' },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
let documentBase64 = ''
|
||||
let documentName = 'document.pdf'
|
||||
|
||||
if (file) {
|
||||
try {
|
||||
const parsed = FileInputSchema.parse(file)
|
||||
const userFiles = processFilesToUserFiles([parsed as RawFileInput], 'docusign-send', logger)
|
||||
if (userFiles.length > 0) {
|
||||
const userFile = userFiles[0]
|
||||
const buffer = await downloadFileFromStorage(userFile, 'docusign-send', logger)
|
||||
documentBase64 = buffer.toString('base64')
|
||||
documentName = userFile.name
|
||||
}
|
||||
} catch (fileError) {
|
||||
logger.error('Failed to process file for DocuSign envelope', { fileError })
|
||||
return NextResponse.json(
|
||||
{ success: false, error: 'Failed to process uploaded file' },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const envelopeBody: Record<string, unknown> = {
|
||||
emailSubject,
|
||||
status: (status as string) || 'sent',
|
||||
recipients: {
|
||||
signers: [
|
||||
{
|
||||
email: signerEmail,
|
||||
name: signerName,
|
||||
recipientId: '1',
|
||||
routingOrder: '1',
|
||||
tabs: {
|
||||
signHereTabs: [
|
||||
{
|
||||
anchorString: '/sig1/',
|
||||
anchorUnits: 'pixels',
|
||||
anchorXOffset: '0',
|
||||
anchorYOffset: '0',
|
||||
},
|
||||
],
|
||||
dateSignedTabs: [
|
||||
{
|
||||
anchorString: '/date1/',
|
||||
anchorUnits: 'pixels',
|
||||
anchorXOffset: '0',
|
||||
anchorYOffset: '0',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
carbonCopies: ccEmail
|
||||
? [
|
||||
{
|
||||
email: ccEmail,
|
||||
name: ccName || (ccEmail as string),
|
||||
recipientId: '2',
|
||||
routingOrder: '2',
|
||||
},
|
||||
]
|
||||
: [],
|
||||
},
|
||||
}
|
||||
|
||||
if (emailBody) {
|
||||
envelopeBody.emailBlurb = emailBody
|
||||
}
|
||||
|
||||
if (documentBase64) {
|
||||
envelopeBody.documents = [
|
||||
{
|
||||
documentBase64,
|
||||
name: documentName,
|
||||
fileExtension: documentName.split('.').pop() || 'pdf',
|
||||
documentId: '1',
|
||||
},
|
||||
]
|
||||
} else if (((status as string) || 'sent') === 'sent') {
|
||||
return NextResponse.json(
|
||||
{ success: false, error: 'A document file is required to send an envelope' },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
const response = await fetch(`${apiBase}/envelopes`, {
|
||||
method: 'POST',
|
||||
headers,
|
||||
body: JSON.stringify(envelopeBody),
|
||||
})
|
||||
|
||||
const data = await response.json()
|
||||
if (!response.ok) {
|
||||
logger.error('DocuSign send envelope failed', { data, status: response.status })
|
||||
return NextResponse.json(
|
||||
{ success: false, error: data.message || data.errorCode || 'Failed to send envelope' },
|
||||
{ status: response.status }
|
||||
)
|
||||
}
|
||||
|
||||
return NextResponse.json(data)
|
||||
}
|
||||
|
||||
async function handleCreateFromTemplate(
|
||||
apiBase: string,
|
||||
headers: Record<string, string>,
|
||||
params: Record<string, unknown>
|
||||
) {
|
||||
const { templateId, emailSubject, emailBody, templateRoles, status } = params
|
||||
|
||||
if (!templateId) {
|
||||
return NextResponse.json({ success: false, error: 'templateId is required' }, { status: 400 })
|
||||
}
|
||||
|
||||
let parsedRoles: unknown[] = []
|
||||
if (templateRoles) {
|
||||
if (typeof templateRoles === 'string') {
|
||||
try {
|
||||
parsedRoles = JSON.parse(templateRoles)
|
||||
} catch {
|
||||
return NextResponse.json(
|
||||
{ success: false, error: 'Invalid JSON for templateRoles' },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
} else if (Array.isArray(templateRoles)) {
|
||||
parsedRoles = templateRoles
|
||||
}
|
||||
}
|
||||
|
||||
const envelopeBody: Record<string, unknown> = {
|
||||
templateId,
|
||||
status: (status as string) || 'sent',
|
||||
templateRoles: parsedRoles,
|
||||
}
|
||||
|
||||
if (emailSubject) envelopeBody.emailSubject = emailSubject
|
||||
if (emailBody) envelopeBody.emailBlurb = emailBody
|
||||
|
||||
const response = await fetch(`${apiBase}/envelopes`, {
|
||||
method: 'POST',
|
||||
headers,
|
||||
body: JSON.stringify(envelopeBody),
|
||||
})
|
||||
|
||||
const data = await response.json()
|
||||
if (!response.ok) {
|
||||
logger.error('DocuSign create from template failed', { data, status: response.status })
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: data.message || data.errorCode || 'Failed to create envelope from template',
|
||||
},
|
||||
{ status: response.status }
|
||||
)
|
||||
}
|
||||
|
||||
return NextResponse.json(data)
|
||||
}
|
||||
|
||||
async function handleGetEnvelope(
|
||||
apiBase: string,
|
||||
headers: Record<string, string>,
|
||||
params: Record<string, unknown>
|
||||
) {
|
||||
const { envelopeId } = params
|
||||
if (!envelopeId) {
|
||||
return NextResponse.json({ success: false, error: 'envelopeId is required' }, { status: 400 })
|
||||
}
|
||||
|
||||
const response = await fetch(
|
||||
`${apiBase}/envelopes/${(envelopeId as string).trim()}?include=recipients,documents`,
|
||||
{ headers }
|
||||
)
|
||||
const data = await response.json()
|
||||
|
||||
if (!response.ok) {
|
||||
return NextResponse.json(
|
||||
{ success: false, error: data.message || data.errorCode || 'Failed to get envelope' },
|
||||
{ status: response.status }
|
||||
)
|
||||
}
|
||||
|
||||
return NextResponse.json(data)
|
||||
}
|
||||
|
||||
async function handleListEnvelopes(
|
||||
apiBase: string,
|
||||
headers: Record<string, string>,
|
||||
params: Record<string, unknown>
|
||||
) {
|
||||
const queryParams = new URLSearchParams()
|
||||
|
||||
const fromDate = params.fromDate as string | undefined
|
||||
if (fromDate) {
|
||||
queryParams.append('from_date', fromDate)
|
||||
} else {
|
||||
const thirtyDaysAgo = new Date()
|
||||
thirtyDaysAgo.setDate(thirtyDaysAgo.getDate() - 30)
|
||||
queryParams.append('from_date', thirtyDaysAgo.toISOString())
|
||||
}
|
||||
|
||||
if (params.toDate) queryParams.append('to_date', params.toDate as string)
|
||||
if (params.envelopeStatus) queryParams.append('status', params.envelopeStatus as string)
|
||||
if (params.searchText) queryParams.append('search_text', params.searchText as string)
|
||||
if (params.count) queryParams.append('count', params.count as string)
|
||||
|
||||
const response = await fetch(`${apiBase}/envelopes?${queryParams}`, { headers })
|
||||
const data = await response.json()
|
||||
|
||||
if (!response.ok) {
|
||||
return NextResponse.json(
|
||||
{ success: false, error: data.message || data.errorCode || 'Failed to list envelopes' },
|
||||
{ status: response.status }
|
||||
)
|
||||
}
|
||||
|
||||
return NextResponse.json(data)
|
||||
}
|
||||
|
||||
async function handleVoidEnvelope(
|
||||
apiBase: string,
|
||||
headers: Record<string, string>,
|
||||
params: Record<string, unknown>
|
||||
) {
|
||||
const { envelopeId, voidedReason } = params
|
||||
if (!envelopeId) {
|
||||
return NextResponse.json({ success: false, error: 'envelopeId is required' }, { status: 400 })
|
||||
}
|
||||
if (!voidedReason) {
|
||||
return NextResponse.json({ success: false, error: 'voidedReason is required' }, { status: 400 })
|
||||
}
|
||||
|
||||
const response = await fetch(`${apiBase}/envelopes/${(envelopeId as string).trim()}`, {
|
||||
method: 'PUT',
|
||||
headers,
|
||||
body: JSON.stringify({ status: 'voided', voidedReason }),
|
||||
})
|
||||
|
||||
const data = await response.json()
|
||||
if (!response.ok) {
|
||||
return NextResponse.json(
|
||||
{ success: false, error: data.message || data.errorCode || 'Failed to void envelope' },
|
||||
{ status: response.status }
|
||||
)
|
||||
}
|
||||
|
||||
return NextResponse.json({ envelopeId, status: 'voided' })
|
||||
}
|
||||
|
||||
async function handleDownloadDocument(
|
||||
apiBase: string,
|
||||
headers: Record<string, string>,
|
||||
params: Record<string, unknown>
|
||||
) {
|
||||
const { envelopeId, documentId } = params
|
||||
if (!envelopeId) {
|
||||
return NextResponse.json({ success: false, error: 'envelopeId is required' }, { status: 400 })
|
||||
}
|
||||
|
||||
const docId = (documentId as string) || 'combined'
|
||||
|
||||
const response = await fetch(
|
||||
`${apiBase}/envelopes/${(envelopeId as string).trim()}/documents/${docId}`,
|
||||
{
|
||||
headers: { Authorization: headers.Authorization },
|
||||
}
|
||||
)
|
||||
|
||||
if (!response.ok) {
|
||||
let errorText = ''
|
||||
try {
|
||||
errorText = await response.text()
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
return NextResponse.json(
|
||||
{ success: false, error: `Failed to download document: ${response.status} ${errorText}` },
|
||||
{ status: response.status }
|
||||
)
|
||||
}
|
||||
|
||||
const contentType = response.headers.get('content-type') || 'application/pdf'
|
||||
const contentDisposition = response.headers.get('content-disposition') || ''
|
||||
let fileName = `document-${docId}.pdf`
|
||||
|
||||
const filenameMatch = contentDisposition.match(/filename[^;=\n]*=((['"]).*?\2|[^;\n]*)/)
|
||||
if (filenameMatch) {
|
||||
fileName = filenameMatch[1].replace(/['"]/g, '')
|
||||
}
|
||||
|
||||
const buffer = Buffer.from(await response.arrayBuffer())
|
||||
const base64Content = buffer.toString('base64')
|
||||
|
||||
return NextResponse.json({ base64Content, mimeType: contentType, fileName })
|
||||
}
|
||||
|
||||
async function handleListTemplates(
|
||||
apiBase: string,
|
||||
headers: Record<string, string>,
|
||||
params: Record<string, unknown>
|
||||
) {
|
||||
const queryParams = new URLSearchParams()
|
||||
if (params.searchText) queryParams.append('search_text', params.searchText as string)
|
||||
if (params.count) queryParams.append('count', params.count as string)
|
||||
|
||||
const queryString = queryParams.toString()
|
||||
const url = queryString ? `${apiBase}/templates?${queryString}` : `${apiBase}/templates`
|
||||
|
||||
const response = await fetch(url, { headers })
|
||||
const data = await response.json()
|
||||
|
||||
if (!response.ok) {
|
||||
return NextResponse.json(
|
||||
{ success: false, error: data.message || data.errorCode || 'Failed to list templates' },
|
||||
{ status: response.status }
|
||||
)
|
||||
}
|
||||
|
||||
return NextResponse.json(data)
|
||||
}
|
||||
|
||||
async function handleListRecipients(
|
||||
apiBase: string,
|
||||
headers: Record<string, string>,
|
||||
params: Record<string, unknown>
|
||||
) {
|
||||
const { envelopeId } = params
|
||||
if (!envelopeId) {
|
||||
return NextResponse.json({ success: false, error: 'envelopeId is required' }, { status: 400 })
|
||||
}
|
||||
|
||||
const response = await fetch(`${apiBase}/envelopes/${(envelopeId as string).trim()}/recipients`, {
|
||||
headers,
|
||||
})
|
||||
const data = await response.json()
|
||||
|
||||
if (!response.ok) {
|
||||
return NextResponse.json(
|
||||
{ success: false, error: data.message || data.errorCode || 'Failed to list recipients' },
|
||||
{ status: response.status }
|
||||
)
|
||||
}
|
||||
|
||||
return NextResponse.json(data)
|
||||
}
|
||||
67
apps/sim/app/api/tools/workday/assign-onboarding/route.ts
Normal file
67
apps/sim/app/api/tools/workday/assign-onboarding/route.ts
Normal file
@@ -0,0 +1,67 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { z } from 'zod'
|
||||
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
||||
import { generateRequestId } from '@/lib/core/utils/request'
|
||||
import { createWorkdaySoapClient, extractRefId, wdRef } from '@/tools/workday/soap'
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
const logger = createLogger('WorkdayAssignOnboardingAPI')
|
||||
|
||||
const RequestSchema = z.object({
|
||||
tenantUrl: z.string().min(1),
|
||||
tenant: z.string().min(1),
|
||||
username: z.string().min(1),
|
||||
password: z.string().min(1),
|
||||
workerId: z.string().min(1),
|
||||
onboardingPlanId: z.string().min(1),
|
||||
actionEventId: z.string().min(1),
|
||||
})
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
const requestId = generateRequestId()
|
||||
|
||||
try {
|
||||
const authResult = await checkInternalAuth(request, { requireWorkflowId: false })
|
||||
if (!authResult.success) {
|
||||
return NextResponse.json({ success: false, error: 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
const body = await request.json()
|
||||
const data = RequestSchema.parse(body)
|
||||
|
||||
const client = await createWorkdaySoapClient(
|
||||
data.tenantUrl,
|
||||
data.tenant,
|
||||
'humanResources',
|
||||
data.username,
|
||||
data.password
|
||||
)
|
||||
|
||||
const [result] = await client.Put_Onboarding_Plan_AssignmentAsync({
|
||||
Onboarding_Plan_Assignment_Data: {
|
||||
Onboarding_Plan_Reference: wdRef('Onboarding_Plan_ID', data.onboardingPlanId),
|
||||
Person_Reference: wdRef('WID', data.workerId),
|
||||
Action_Event_Reference: wdRef('Background_Check_ID', data.actionEventId),
|
||||
Assignment_Effective_Moment: new Date().toISOString(),
|
||||
Active: true,
|
||||
},
|
||||
})
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
output: {
|
||||
assignmentId: extractRefId(result?.Onboarding_Plan_Assignment_Reference),
|
||||
workerId: data.workerId,
|
||||
planId: data.onboardingPlanId,
|
||||
},
|
||||
})
|
||||
} catch (error) {
|
||||
logger.error(`[${requestId}] Workday assign onboarding failed`, { error })
|
||||
return NextResponse.json(
|
||||
{ success: false, error: error instanceof Error ? error.message : 'Unknown error' },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
94
apps/sim/app/api/tools/workday/change-job/route.ts
Normal file
94
apps/sim/app/api/tools/workday/change-job/route.ts
Normal file
@@ -0,0 +1,94 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { z } from 'zod'
|
||||
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
||||
import { generateRequestId } from '@/lib/core/utils/request'
|
||||
import { createWorkdaySoapClient, extractRefId, wdRef } from '@/tools/workday/soap'
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
const logger = createLogger('WorkdayChangeJobAPI')
|
||||
|
||||
const RequestSchema = z.object({
|
||||
tenantUrl: z.string().min(1),
|
||||
tenant: z.string().min(1),
|
||||
username: z.string().min(1),
|
||||
password: z.string().min(1),
|
||||
workerId: z.string().min(1),
|
||||
effectiveDate: z.string().min(1),
|
||||
newPositionId: z.string().optional(),
|
||||
newJobProfileId: z.string().optional(),
|
||||
newLocationId: z.string().optional(),
|
||||
newSupervisoryOrgId: z.string().optional(),
|
||||
reason: z.string().min(1, 'Reason is required for job changes'),
|
||||
})
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
const requestId = generateRequestId()
|
||||
|
||||
try {
|
||||
const authResult = await checkInternalAuth(request, { requireWorkflowId: false })
|
||||
if (!authResult.success) {
|
||||
return NextResponse.json({ success: false, error: 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
const body = await request.json()
|
||||
const data = RequestSchema.parse(body)
|
||||
|
||||
const changeJobDetailData: Record<string, unknown> = {
|
||||
Reason_Reference: wdRef('Change_Job_Subcategory_ID', data.reason),
|
||||
}
|
||||
if (data.newPositionId) {
|
||||
changeJobDetailData.Position_Reference = wdRef('Position_ID', data.newPositionId)
|
||||
}
|
||||
if (data.newJobProfileId) {
|
||||
changeJobDetailData.Job_Profile_Reference = wdRef('Job_Profile_ID', data.newJobProfileId)
|
||||
}
|
||||
if (data.newLocationId) {
|
||||
changeJobDetailData.Location_Reference = wdRef('Location_ID', data.newLocationId)
|
||||
}
|
||||
if (data.newSupervisoryOrgId) {
|
||||
changeJobDetailData.Supervisory_Organization_Reference = wdRef(
|
||||
'Supervisory_Organization_ID',
|
||||
data.newSupervisoryOrgId
|
||||
)
|
||||
}
|
||||
|
||||
const client = await createWorkdaySoapClient(
|
||||
data.tenantUrl,
|
||||
data.tenant,
|
||||
'staffing',
|
||||
data.username,
|
||||
data.password
|
||||
)
|
||||
|
||||
const [result] = await client.Change_JobAsync({
|
||||
Business_Process_Parameters: {
|
||||
Auto_Complete: true,
|
||||
Run_Now: true,
|
||||
},
|
||||
Change_Job_Data: {
|
||||
Worker_Reference: wdRef('Employee_ID', data.workerId),
|
||||
Effective_Date: data.effectiveDate,
|
||||
Change_Job_Detail_Data: changeJobDetailData,
|
||||
},
|
||||
})
|
||||
|
||||
const eventRef = result?.Event_Reference
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
output: {
|
||||
eventId: extractRefId(eventRef),
|
||||
workerId: data.workerId,
|
||||
effectiveDate: data.effectiveDate,
|
||||
},
|
||||
})
|
||||
} catch (error) {
|
||||
logger.error(`[${requestId}] Workday change job failed`, { error })
|
||||
return NextResponse.json(
|
||||
{ success: false, error: error instanceof Error ? error.message : 'Unknown error' },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
134
apps/sim/app/api/tools/workday/create-prehire/route.ts
Normal file
134
apps/sim/app/api/tools/workday/create-prehire/route.ts
Normal file
@@ -0,0 +1,134 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { z } from 'zod'
|
||||
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
||||
import { generateRequestId } from '@/lib/core/utils/request'
|
||||
import { createWorkdaySoapClient, extractRefId, wdRef } from '@/tools/workday/soap'
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
const logger = createLogger('WorkdayCreatePrehireAPI')
|
||||
|
||||
const RequestSchema = z.object({
|
||||
tenantUrl: z.string().min(1),
|
||||
tenant: z.string().min(1),
|
||||
username: z.string().min(1),
|
||||
password: z.string().min(1),
|
||||
legalName: z.string().min(1),
|
||||
email: z.string().optional(),
|
||||
phoneNumber: z.string().optional(),
|
||||
address: z.string().optional(),
|
||||
countryCode: z.string().optional(),
|
||||
})
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
const requestId = generateRequestId()
|
||||
|
||||
try {
|
||||
const authResult = await checkInternalAuth(request, { requireWorkflowId: false })
|
||||
if (!authResult.success) {
|
||||
return NextResponse.json({ success: false, error: 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
const body = await request.json()
|
||||
const data = RequestSchema.parse(body)
|
||||
|
||||
if (!data.email && !data.phoneNumber && !data.address) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: 'At least one contact method (email, phone, or address) is required',
|
||||
},
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
const parts = data.legalName.trim().split(/\s+/)
|
||||
const firstName = parts[0] ?? ''
|
||||
const lastName = parts.length > 1 ? parts.slice(1).join(' ') : ''
|
||||
|
||||
if (!lastName) {
|
||||
return NextResponse.json(
|
||||
{ success: false, error: 'Legal name must include both a first name and last name' },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
const client = await createWorkdaySoapClient(
|
||||
data.tenantUrl,
|
||||
data.tenant,
|
||||
'staffing',
|
||||
data.username,
|
||||
data.password
|
||||
)
|
||||
|
||||
const contactData: Record<string, unknown> = {}
|
||||
if (data.email) {
|
||||
contactData.Email_Address_Data = [
|
||||
{
|
||||
Email_Address: data.email,
|
||||
Usage_Data: {
|
||||
Type_Data: { Type_Reference: wdRef('Communication_Usage_Type_ID', 'WORK') },
|
||||
Public: true,
|
||||
},
|
||||
},
|
||||
]
|
||||
}
|
||||
if (data.phoneNumber) {
|
||||
contactData.Phone_Data = [
|
||||
{
|
||||
Phone_Number: data.phoneNumber,
|
||||
Phone_Device_Type_Reference: wdRef('Phone_Device_Type_ID', 'Landline'),
|
||||
Usage_Data: {
|
||||
Type_Data: { Type_Reference: wdRef('Communication_Usage_Type_ID', 'WORK') },
|
||||
Public: true,
|
||||
},
|
||||
},
|
||||
]
|
||||
}
|
||||
if (data.address) {
|
||||
contactData.Address_Data = [
|
||||
{
|
||||
Formatted_Address: data.address,
|
||||
Usage_Data: {
|
||||
Type_Data: { Type_Reference: wdRef('Communication_Usage_Type_ID', 'WORK') },
|
||||
Public: true,
|
||||
},
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
const [result] = await client.Put_ApplicantAsync({
|
||||
Applicant_Data: {
|
||||
Personal_Data: {
|
||||
Name_Data: {
|
||||
Legal_Name_Data: {
|
||||
Name_Detail_Data: {
|
||||
Country_Reference: wdRef('ISO_3166-1_Alpha-2_Code', data.countryCode ?? 'US'),
|
||||
First_Name: firstName,
|
||||
Last_Name: lastName,
|
||||
},
|
||||
},
|
||||
},
|
||||
Contact_Information_Data: contactData,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
const applicantRef = result?.Applicant_Reference
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
output: {
|
||||
preHireId: extractRefId(applicantRef),
|
||||
descriptor: applicantRef?.attributes?.Descriptor ?? null,
|
||||
},
|
||||
})
|
||||
} catch (error) {
|
||||
logger.error(`[${requestId}] Workday create prehire failed`, { error })
|
||||
return NextResponse.json(
|
||||
{ success: false, error: error instanceof Error ? error.message : 'Unknown error' },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
101
apps/sim/app/api/tools/workday/get-compensation/route.ts
Normal file
101
apps/sim/app/api/tools/workday/get-compensation/route.ts
Normal file
@@ -0,0 +1,101 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { z } from 'zod'
|
||||
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
||||
import { generateRequestId } from '@/lib/core/utils/request'
|
||||
import {
|
||||
createWorkdaySoapClient,
|
||||
extractRefId,
|
||||
normalizeSoapArray,
|
||||
type WorkdayCompensationDataSoap,
|
||||
type WorkdayCompensationPlanSoap,
|
||||
type WorkdayWorkerSoap,
|
||||
} from '@/tools/workday/soap'
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
const logger = createLogger('WorkdayGetCompensationAPI')
|
||||
|
||||
const RequestSchema = z.object({
|
||||
tenantUrl: z.string().min(1),
|
||||
tenant: z.string().min(1),
|
||||
username: z.string().min(1),
|
||||
password: z.string().min(1),
|
||||
workerId: z.string().min(1),
|
||||
})
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
const requestId = generateRequestId()
|
||||
|
||||
try {
|
||||
const authResult = await checkInternalAuth(request, { requireWorkflowId: false })
|
||||
if (!authResult.success) {
|
||||
return NextResponse.json({ success: false, error: 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
const body = await request.json()
|
||||
const data = RequestSchema.parse(body)
|
||||
|
||||
const client = await createWorkdaySoapClient(
|
||||
data.tenantUrl,
|
||||
data.tenant,
|
||||
'humanResources',
|
||||
data.username,
|
||||
data.password
|
||||
)
|
||||
|
||||
const [result] = await client.Get_WorkersAsync({
|
||||
Request_References: {
|
||||
Worker_Reference: {
|
||||
ID: { attributes: { 'wd:type': 'Employee_ID' }, $value: data.workerId },
|
||||
},
|
||||
},
|
||||
Response_Group: {
|
||||
Include_Reference: true,
|
||||
Include_Compensation: true,
|
||||
},
|
||||
})
|
||||
|
||||
const worker =
|
||||
normalizeSoapArray(
|
||||
result?.Response_Data?.Worker as WorkdayWorkerSoap | WorkdayWorkerSoap[] | undefined
|
||||
)[0] ?? null
|
||||
const compensationData = worker?.Worker_Data?.Compensation_Data
|
||||
|
||||
const mapPlan = (p: WorkdayCompensationPlanSoap) => ({
|
||||
id: extractRefId(p.Compensation_Plan_Reference) ?? null,
|
||||
planName: p.Compensation_Plan_Reference?.attributes?.Descriptor ?? null,
|
||||
amount: p.Amount ?? p.Per_Unit_Amount ?? p.Individual_Target_Amount ?? null,
|
||||
currency: extractRefId(p.Currency_Reference) ?? null,
|
||||
frequency: extractRefId(p.Frequency_Reference) ?? null,
|
||||
})
|
||||
|
||||
const planTypeKeys: (keyof WorkdayCompensationDataSoap)[] = [
|
||||
'Employee_Base_Pay_Plan_Assignment_Data',
|
||||
'Employee_Salary_Unit_Plan_Assignment_Data',
|
||||
'Employee_Bonus_Plan_Assignment_Data',
|
||||
'Employee_Allowance_Plan_Assignment_Data',
|
||||
'Employee_Commission_Plan_Assignment_Data',
|
||||
'Employee_Stock_Plan_Assignment_Data',
|
||||
'Employee_Period_Salary_Plan_Assignment_Data',
|
||||
]
|
||||
|
||||
const compensationPlans: ReturnType<typeof mapPlan>[] = []
|
||||
for (const key of planTypeKeys) {
|
||||
for (const plan of normalizeSoapArray(compensationData?.[key])) {
|
||||
compensationPlans.push(mapPlan(plan))
|
||||
}
|
||||
}
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
output: { compensationPlans },
|
||||
})
|
||||
} catch (error) {
|
||||
logger.error(`[${requestId}] Workday get compensation failed`, { error })
|
||||
return NextResponse.json(
|
||||
{ success: false, error: error instanceof Error ? error.message : 'Unknown error' },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
94
apps/sim/app/api/tools/workday/get-organizations/route.ts
Normal file
94
apps/sim/app/api/tools/workday/get-organizations/route.ts
Normal file
@@ -0,0 +1,94 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { z } from 'zod'
|
||||
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
||||
import { generateRequestId } from '@/lib/core/utils/request'
|
||||
import {
|
||||
createWorkdaySoapClient,
|
||||
extractRefId,
|
||||
normalizeSoapArray,
|
||||
type WorkdayOrganizationSoap,
|
||||
} from '@/tools/workday/soap'
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
const logger = createLogger('WorkdayGetOrganizationsAPI')
|
||||
|
||||
const RequestSchema = z.object({
|
||||
tenantUrl: z.string().min(1),
|
||||
tenant: z.string().min(1),
|
||||
username: z.string().min(1),
|
||||
password: z.string().min(1),
|
||||
type: z.string().optional(),
|
||||
limit: z.number().optional(),
|
||||
offset: z.number().optional(),
|
||||
})
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
const requestId = generateRequestId()
|
||||
|
||||
try {
|
||||
const authResult = await checkInternalAuth(request, { requireWorkflowId: false })
|
||||
if (!authResult.success) {
|
||||
return NextResponse.json({ success: false, error: 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
const body = await request.json()
|
||||
const data = RequestSchema.parse(body)
|
||||
|
||||
const client = await createWorkdaySoapClient(
|
||||
data.tenantUrl,
|
||||
data.tenant,
|
||||
'humanResources',
|
||||
data.username,
|
||||
data.password
|
||||
)
|
||||
|
||||
const limit = data.limit ?? 20
|
||||
const offset = data.offset ?? 0
|
||||
const page = offset > 0 ? Math.floor(offset / limit) + 1 : 1
|
||||
|
||||
const [result] = await client.Get_OrganizationsAsync({
|
||||
Response_Filter: { Page: page, Count: limit },
|
||||
Request_Criteria: data.type
|
||||
? {
|
||||
Organization_Type_Reference: {
|
||||
ID: {
|
||||
attributes: { 'wd:type': 'Organization_Type_ID' },
|
||||
$value: data.type,
|
||||
},
|
||||
},
|
||||
}
|
||||
: undefined,
|
||||
Response_Group: { Include_Hierarchy_Data: true },
|
||||
})
|
||||
|
||||
const orgsArray = normalizeSoapArray(
|
||||
result?.Response_Data?.Organization as
|
||||
| WorkdayOrganizationSoap
|
||||
| WorkdayOrganizationSoap[]
|
||||
| undefined
|
||||
)
|
||||
|
||||
const organizations = orgsArray.map((o) => ({
|
||||
id: extractRefId(o.Organization_Reference) ?? null,
|
||||
descriptor: o.Organization_Descriptor ?? null,
|
||||
type: extractRefId(o.Organization_Data?.Organization_Type_Reference) ?? null,
|
||||
subtype: extractRefId(o.Organization_Data?.Organization_Subtype_Reference) ?? null,
|
||||
isActive: o.Organization_Data?.Inactive != null ? !o.Organization_Data.Inactive : null,
|
||||
}))
|
||||
|
||||
const total = result?.Response_Results?.Total_Results ?? organizations.length
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
output: { organizations, total },
|
||||
})
|
||||
} catch (error) {
|
||||
logger.error(`[${requestId}] Workday get organizations failed`, { error })
|
||||
return NextResponse.json(
|
||||
{ success: false, error: error instanceof Error ? error.message : 'Unknown error' },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
87
apps/sim/app/api/tools/workday/get-worker/route.ts
Normal file
87
apps/sim/app/api/tools/workday/get-worker/route.ts
Normal file
@@ -0,0 +1,87 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { z } from 'zod'
|
||||
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
||||
import { generateRequestId } from '@/lib/core/utils/request'
|
||||
import {
|
||||
createWorkdaySoapClient,
|
||||
extractRefId,
|
||||
normalizeSoapArray,
|
||||
type WorkdayWorkerSoap,
|
||||
} from '@/tools/workday/soap'
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
const logger = createLogger('WorkdayGetWorkerAPI')
|
||||
|
||||
const RequestSchema = z.object({
|
||||
tenantUrl: z.string().min(1),
|
||||
tenant: z.string().min(1),
|
||||
username: z.string().min(1),
|
||||
password: z.string().min(1),
|
||||
workerId: z.string().min(1),
|
||||
})
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
const requestId = generateRequestId()
|
||||
|
||||
try {
|
||||
const authResult = await checkInternalAuth(request, { requireWorkflowId: false })
|
||||
if (!authResult.success) {
|
||||
return NextResponse.json({ success: false, error: 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
const body = await request.json()
|
||||
const data = RequestSchema.parse(body)
|
||||
|
||||
const client = await createWorkdaySoapClient(
|
||||
data.tenantUrl,
|
||||
data.tenant,
|
||||
'humanResources',
|
||||
data.username,
|
||||
data.password
|
||||
)
|
||||
|
||||
const [result] = await client.Get_WorkersAsync({
|
||||
Request_References: {
|
||||
Worker_Reference: {
|
||||
ID: { attributes: { 'wd:type': 'Employee_ID' }, $value: data.workerId },
|
||||
},
|
||||
},
|
||||
Response_Group: {
|
||||
Include_Reference: true,
|
||||
Include_Personal_Information: true,
|
||||
Include_Employment_Information: true,
|
||||
Include_Compensation: true,
|
||||
Include_Organizations: true,
|
||||
},
|
||||
})
|
||||
|
||||
const worker =
|
||||
normalizeSoapArray(
|
||||
result?.Response_Data?.Worker as WorkdayWorkerSoap | WorkdayWorkerSoap[] | undefined
|
||||
)[0] ?? null
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
output: {
|
||||
worker: worker
|
||||
? {
|
||||
id: extractRefId(worker.Worker_Reference) ?? null,
|
||||
descriptor: worker.Worker_Descriptor ?? null,
|
||||
personalData: worker.Worker_Data?.Personal_Data ?? null,
|
||||
employmentData: worker.Worker_Data?.Employment_Data ?? null,
|
||||
compensationData: worker.Worker_Data?.Compensation_Data ?? null,
|
||||
organizationData: worker.Worker_Data?.Organization_Data ?? null,
|
||||
}
|
||||
: null,
|
||||
},
|
||||
})
|
||||
} catch (error) {
|
||||
logger.error(`[${requestId}] Workday get worker failed`, { error })
|
||||
return NextResponse.json(
|
||||
{ success: false, error: error instanceof Error ? error.message : 'Unknown error' },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
78
apps/sim/app/api/tools/workday/hire/route.ts
Normal file
78
apps/sim/app/api/tools/workday/hire/route.ts
Normal file
@@ -0,0 +1,78 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { z } from 'zod'
|
||||
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
||||
import { generateRequestId } from '@/lib/core/utils/request'
|
||||
import { createWorkdaySoapClient, extractRefId, wdRef } from '@/tools/workday/soap'
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
const logger = createLogger('WorkdayHireAPI')
|
||||
|
||||
const RequestSchema = z.object({
|
||||
tenantUrl: z.string().min(1),
|
||||
tenant: z.string().min(1),
|
||||
username: z.string().min(1),
|
||||
password: z.string().min(1),
|
||||
preHireId: z.string().min(1),
|
||||
positionId: z.string().min(1),
|
||||
hireDate: z.string().min(1),
|
||||
employeeType: z.string().optional(),
|
||||
})
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
const requestId = generateRequestId()
|
||||
|
||||
try {
|
||||
const authResult = await checkInternalAuth(request, { requireWorkflowId: false })
|
||||
if (!authResult.success) {
|
||||
return NextResponse.json({ success: false, error: 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
const body = await request.json()
|
||||
const data = RequestSchema.parse(body)
|
||||
|
||||
const client = await createWorkdaySoapClient(
|
||||
data.tenantUrl,
|
||||
data.tenant,
|
||||
'staffing',
|
||||
data.username,
|
||||
data.password
|
||||
)
|
||||
|
||||
const [result] = await client.Hire_EmployeeAsync({
|
||||
Business_Process_Parameters: {
|
||||
Auto_Complete: true,
|
||||
Run_Now: true,
|
||||
},
|
||||
Hire_Employee_Data: {
|
||||
Applicant_Reference: wdRef('Applicant_ID', data.preHireId),
|
||||
Position_Reference: wdRef('Position_ID', data.positionId),
|
||||
Hire_Date: data.hireDate,
|
||||
Hire_Employee_Event_Data: {
|
||||
Employee_Type_Reference: wdRef('Employee_Type_ID', data.employeeType ?? 'Regular'),
|
||||
First_Day_of_Work: data.hireDate,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
const employeeRef = result?.Employee_Reference
|
||||
const eventRef = result?.Event_Reference
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
output: {
|
||||
workerId: extractRefId(employeeRef),
|
||||
employeeId: extractRefId(employeeRef),
|
||||
eventId: extractRefId(eventRef),
|
||||
hireDate: data.hireDate,
|
||||
},
|
||||
})
|
||||
} catch (error) {
|
||||
logger.error(`[${requestId}] Workday hire employee failed`, { error })
|
||||
return NextResponse.json(
|
||||
{ success: false, error: error instanceof Error ? error.message : 'Unknown error' },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
83
apps/sim/app/api/tools/workday/list-workers/route.ts
Normal file
83
apps/sim/app/api/tools/workday/list-workers/route.ts
Normal file
@@ -0,0 +1,83 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { z } from 'zod'
|
||||
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
||||
import { generateRequestId } from '@/lib/core/utils/request'
|
||||
import {
|
||||
createWorkdaySoapClient,
|
||||
extractRefId,
|
||||
normalizeSoapArray,
|
||||
type WorkdayWorkerSoap,
|
||||
} from '@/tools/workday/soap'
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
const logger = createLogger('WorkdayListWorkersAPI')
|
||||
|
||||
const RequestSchema = z.object({
|
||||
tenantUrl: z.string().min(1),
|
||||
tenant: z.string().min(1),
|
||||
username: z.string().min(1),
|
||||
password: z.string().min(1),
|
||||
limit: z.number().optional(),
|
||||
offset: z.number().optional(),
|
||||
})
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
const requestId = generateRequestId()
|
||||
|
||||
try {
|
||||
const authResult = await checkInternalAuth(request, { requireWorkflowId: false })
|
||||
if (!authResult.success) {
|
||||
return NextResponse.json({ success: false, error: 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
const body = await request.json()
|
||||
const data = RequestSchema.parse(body)
|
||||
|
||||
const client = await createWorkdaySoapClient(
|
||||
data.tenantUrl,
|
||||
data.tenant,
|
||||
'humanResources',
|
||||
data.username,
|
||||
data.password
|
||||
)
|
||||
|
||||
const limit = data.limit ?? 20
|
||||
const offset = data.offset ?? 0
|
||||
const page = offset > 0 ? Math.floor(offset / limit) + 1 : 1
|
||||
|
||||
const [result] = await client.Get_WorkersAsync({
|
||||
Response_Filter: { Page: page, Count: limit },
|
||||
Response_Group: {
|
||||
Include_Reference: true,
|
||||
Include_Personal_Information: true,
|
||||
Include_Employment_Information: true,
|
||||
},
|
||||
})
|
||||
|
||||
const workersArray = normalizeSoapArray(
|
||||
result?.Response_Data?.Worker as WorkdayWorkerSoap | WorkdayWorkerSoap[] | undefined
|
||||
)
|
||||
|
||||
const workers = workersArray.map((w) => ({
|
||||
id: extractRefId(w.Worker_Reference) ?? null,
|
||||
descriptor: w.Worker_Descriptor ?? null,
|
||||
personalData: w.Worker_Data?.Personal_Data ?? null,
|
||||
employmentData: w.Worker_Data?.Employment_Data ?? null,
|
||||
}))
|
||||
|
||||
const total = result?.Response_Results?.Total_Results ?? workers.length
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
output: { workers, total },
|
||||
})
|
||||
} catch (error) {
|
||||
logger.error(`[${requestId}] Workday list workers failed`, { error })
|
||||
return NextResponse.json(
|
||||
{ success: false, error: error instanceof Error ? error.message : 'Unknown error' },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
77
apps/sim/app/api/tools/workday/terminate/route.ts
Normal file
77
apps/sim/app/api/tools/workday/terminate/route.ts
Normal file
@@ -0,0 +1,77 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { z } from 'zod'
|
||||
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
||||
import { generateRequestId } from '@/lib/core/utils/request'
|
||||
import { createWorkdaySoapClient, extractRefId, wdRef } from '@/tools/workday/soap'
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
const logger = createLogger('WorkdayTerminateAPI')
|
||||
|
||||
const RequestSchema = z.object({
|
||||
tenantUrl: z.string().min(1),
|
||||
tenant: z.string().min(1),
|
||||
username: z.string().min(1),
|
||||
password: z.string().min(1),
|
||||
workerId: z.string().min(1),
|
||||
terminationDate: z.string().min(1),
|
||||
reason: z.string().min(1),
|
||||
notificationDate: z.string().optional(),
|
||||
lastDayOfWork: z.string().optional(),
|
||||
})
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
const requestId = generateRequestId()
|
||||
|
||||
try {
|
||||
const authResult = await checkInternalAuth(request, { requireWorkflowId: false })
|
||||
if (!authResult.success) {
|
||||
return NextResponse.json({ success: false, error: 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
const body = await request.json()
|
||||
const data = RequestSchema.parse(body)
|
||||
|
||||
const client = await createWorkdaySoapClient(
|
||||
data.tenantUrl,
|
||||
data.tenant,
|
||||
'staffing',
|
||||
data.username,
|
||||
data.password
|
||||
)
|
||||
|
||||
const [result] = await client.Terminate_EmployeeAsync({
|
||||
Business_Process_Parameters: {
|
||||
Auto_Complete: true,
|
||||
Run_Now: true,
|
||||
},
|
||||
Terminate_Employee_Data: {
|
||||
Employee_Reference: wdRef('Employee_ID', data.workerId),
|
||||
Termination_Date: data.terminationDate,
|
||||
Terminate_Event_Data: {
|
||||
Primary_Reason_Reference: wdRef('Termination_Subcategory_ID', data.reason),
|
||||
Last_Day_of_Work: data.lastDayOfWork ?? data.terminationDate,
|
||||
Notification_Date: data.notificationDate ?? data.terminationDate,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
const eventRef = result?.Event_Reference
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
output: {
|
||||
eventId: extractRefId(eventRef),
|
||||
workerId: data.workerId,
|
||||
terminationDate: data.terminationDate,
|
||||
},
|
||||
})
|
||||
} catch (error) {
|
||||
logger.error(`[${requestId}] Workday terminate employee failed`, { error })
|
||||
return NextResponse.json(
|
||||
{ success: false, error: error instanceof Error ? error.message : 'Unknown error' },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
66
apps/sim/app/api/tools/workday/update-worker/route.ts
Normal file
66
apps/sim/app/api/tools/workday/update-worker/route.ts
Normal file
@@ -0,0 +1,66 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { z } from 'zod'
|
||||
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
||||
import { generateRequestId } from '@/lib/core/utils/request'
|
||||
import { createWorkdaySoapClient, extractRefId, wdRef } from '@/tools/workday/soap'
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
const logger = createLogger('WorkdayUpdateWorkerAPI')
|
||||
|
||||
const RequestSchema = z.object({
|
||||
tenantUrl: z.string().min(1),
|
||||
tenant: z.string().min(1),
|
||||
username: z.string().min(1),
|
||||
password: z.string().min(1),
|
||||
workerId: z.string().min(1),
|
||||
fields: z.record(z.unknown()),
|
||||
})
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
const requestId = generateRequestId()
|
||||
|
||||
try {
|
||||
const authResult = await checkInternalAuth(request, { requireWorkflowId: false })
|
||||
if (!authResult.success) {
|
||||
return NextResponse.json({ success: false, error: 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
const body = await request.json()
|
||||
const data = RequestSchema.parse(body)
|
||||
|
||||
const client = await createWorkdaySoapClient(
|
||||
data.tenantUrl,
|
||||
data.tenant,
|
||||
'humanResources',
|
||||
data.username,
|
||||
data.password
|
||||
)
|
||||
|
||||
const [result] = await client.Change_Personal_InformationAsync({
|
||||
Business_Process_Parameters: {
|
||||
Auto_Complete: true,
|
||||
Run_Now: true,
|
||||
},
|
||||
Change_Personal_Information_Data: {
|
||||
Person_Reference: wdRef('Employee_ID', data.workerId),
|
||||
Personal_Information_Data: data.fields,
|
||||
},
|
||||
})
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
output: {
|
||||
eventId: extractRefId(result?.Personal_Information_Change_Event_Reference),
|
||||
workerId: data.workerId,
|
||||
},
|
||||
})
|
||||
} catch (error) {
|
||||
logger.error(`[${requestId}] Workday update worker failed`, { error })
|
||||
return NextResponse.json(
|
||||
{ success: false, error: error instanceof Error ? error.message : 'Unknown error' },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -93,7 +93,7 @@ export async function POST(request: NextRequest, { params }: { params: Promise<{
|
||||
return NextResponse.json({ error: 'No file provided' }, { status: 400 })
|
||||
}
|
||||
|
||||
const fileName = rawFile.name || 'untitled'
|
||||
const fileName = rawFile.name || 'untitled.md'
|
||||
|
||||
const maxSize = 100 * 1024 * 1024
|
||||
if (rawFile.size > maxSize) {
|
||||
|
||||
@@ -151,6 +151,8 @@ export function Files() {
|
||||
}
|
||||
|
||||
const justCreatedFileIdRef = useRef<string | null>(null)
|
||||
const filesRef = useRef(files)
|
||||
filesRef.current = files
|
||||
|
||||
const [uploading, setUploading] = useState(false)
|
||||
const [uploadProgress, setUploadProgress] = useState({ completed: 0, total: 0 })
|
||||
@@ -483,11 +485,11 @@ export function Files() {
|
||||
if (isJustCreated) {
|
||||
setPreviewMode('editor')
|
||||
} else {
|
||||
const file = selectedFileId ? files.find((f) => f.id === selectedFileId) : null
|
||||
const file = selectedFileId ? filesRef.current.find((f) => f.id === selectedFileId) : null
|
||||
const canPreview = file ? isPreviewable(file) : false
|
||||
setPreviewMode(canPreview ? 'preview' : 'editor')
|
||||
}
|
||||
}, [selectedFileId, files])
|
||||
}, [selectedFileId])
|
||||
|
||||
useEffect(() => {
|
||||
if (!selectedFile) return
|
||||
|
||||
@@ -168,7 +168,7 @@ export const SubflowNodeComponent = memo(({ data, id, selected }: NodeProps<Subf
|
||||
<ActionBar blockId={id} blockType={data.kind} disabled={!userPermissions.canEdit} />
|
||||
)}
|
||||
|
||||
{/* Header Section — only interactive area for dragging */}
|
||||
{/* Header Section */}
|
||||
<div
|
||||
onClick={() => setCurrentBlockId(id)}
|
||||
className='workflow-drag-handle flex cursor-grab items-center justify-between rounded-t-[8px] border-[var(--border)] border-b bg-[var(--surface-2)] py-[8px] pr-[12px] pl-[8px] [&:active]:cursor-grabbing'
|
||||
@@ -198,14 +198,15 @@ export const SubflowNodeComponent = memo(({ data, id, selected }: NodeProps<Subf
|
||||
</div>
|
||||
|
||||
{/*
|
||||
* Subflow body background. Uses pointer-events: none so that edges rendered
|
||||
* inside the subflow remain clickable. The subflow node wrapper also has
|
||||
* pointer-events: none (set in workflow.tsx), so body-area clicks pass
|
||||
* through to the pane. Subflow selection is done via the header above.
|
||||
* Subflow body background. Captures clicks to select the subflow in the
|
||||
* panel editor, matching the header click behavior. Child nodes and edges
|
||||
* are rendered as sibling divs at the viewport level by ReactFlow (not as
|
||||
* DOM children), so enabling pointer events here doesn't block them.
|
||||
*/}
|
||||
<div
|
||||
className='absolute inset-0 top-[44px] rounded-b-[8px]'
|
||||
style={{ pointerEvents: 'none' }}
|
||||
className='workflow-drag-handle absolute inset-0 top-[44px] cursor-grab rounded-b-[8px] [&:active]:cursor-grabbing'
|
||||
style={{ pointerEvents: isPreview ? 'none' : 'auto' }}
|
||||
onClick={() => setCurrentBlockId(id)}
|
||||
/>
|
||||
|
||||
{!isPreview && (
|
||||
|
||||
@@ -221,3 +221,68 @@ export function resolveParentChildSelectionConflicts(
|
||||
|
||||
return hasConflict ? resolved : nodes
|
||||
}
|
||||
|
||||
export function getNodeSelectionContextId(
|
||||
node: Pick<Node, 'id' | 'parentId'>,
|
||||
blocks: Record<string, { data?: { parentId?: string } }>
|
||||
): string | null {
|
||||
return node.parentId || blocks[node.id]?.data?.parentId || null
|
||||
}
|
||||
|
||||
export function getEdgeSelectionContextId(
|
||||
edge: Pick<Edge, 'source' | 'target'>,
|
||||
nodes: Array<Pick<Node, 'id' | 'parentId'>>,
|
||||
blocks: Record<string, { data?: { parentId?: string } }>
|
||||
): string | null {
|
||||
const sourceNode = nodes.find((node) => node.id === edge.source)
|
||||
const targetNode = nodes.find((node) => node.id === edge.target)
|
||||
const sourceContextId = sourceNode ? getNodeSelectionContextId(sourceNode, blocks) : null
|
||||
const targetContextId = targetNode ? getNodeSelectionContextId(targetNode, blocks) : null
|
||||
if (sourceContextId) return sourceContextId
|
||||
if (targetContextId) return targetContextId
|
||||
return null
|
||||
}
|
||||
|
||||
export function resolveSelectionContextConflicts(
|
||||
nodes: Node[],
|
||||
blocks: Record<string, { data?: { parentId?: string } }>,
|
||||
preferredContextId?: string | null
|
||||
): Node[] {
|
||||
const selectedNodes = nodes.filter((node) => node.selected)
|
||||
if (selectedNodes.length <= 1) return nodes
|
||||
|
||||
const allowedContextId =
|
||||
preferredContextId !== undefined
|
||||
? preferredContextId
|
||||
: getNodeSelectionContextId(selectedNodes[0], blocks)
|
||||
let hasConflict = false
|
||||
|
||||
const resolved = nodes.map((node) => {
|
||||
if (!node.selected) return node
|
||||
const contextId = getNodeSelectionContextId(node, blocks)
|
||||
if (contextId !== allowedContextId) {
|
||||
hasConflict = true
|
||||
return { ...node, selected: false }
|
||||
}
|
||||
return node
|
||||
})
|
||||
|
||||
return hasConflict ? resolved : nodes
|
||||
}
|
||||
|
||||
export function resolveSelectionConflicts(
|
||||
nodes: Node[],
|
||||
blocks: Record<string, { data?: { parentId?: string } }>,
|
||||
preferredNodeId?: string
|
||||
): Node[] {
|
||||
const afterParentChild = resolveParentChildSelectionConflicts(nodes, blocks)
|
||||
|
||||
const preferredContextId =
|
||||
preferredNodeId !== undefined
|
||||
? afterParentChild.find((n) => n.id === preferredNodeId && n.selected)
|
||||
? getNodeSelectionContextId(afterParentChild.find((n) => n.id === preferredNodeId)!, blocks)
|
||||
: undefined
|
||||
: undefined
|
||||
|
||||
return resolveSelectionContextConflicts(afterParentChild, blocks, preferredContextId)
|
||||
}
|
||||
|
||||
@@ -59,11 +59,13 @@ import {
|
||||
filterProtectedBlocks,
|
||||
getClampedPositionForNode,
|
||||
getDescendantBlockIds,
|
||||
getEdgeSelectionContextId,
|
||||
getNodeSelectionContextId,
|
||||
getWorkflowLockToggleIds,
|
||||
isBlockProtected,
|
||||
isEdgeProtected,
|
||||
isInEditableElement,
|
||||
resolveParentChildSelectionConflicts,
|
||||
resolveSelectionConflicts,
|
||||
validateTriggerPaste,
|
||||
} from '@/app/workspace/[workspaceId]/w/[workflowId]/utils'
|
||||
import { useSocket } from '@/app/workspace/providers/socket-provider'
|
||||
@@ -168,16 +170,17 @@ function mapEdgesByNode(edges: Edge[], nodeIds: Set<string>): Map<string, Edge[]
|
||||
|
||||
/**
|
||||
* Syncs the panel editor with the current selection state.
|
||||
* Shows block details when exactly one block is selected, clears otherwise.
|
||||
* Shows the last selected block in the panel. Clears when nothing is selected.
|
||||
*/
|
||||
function syncPanelWithSelection(selectedIds: string[]) {
|
||||
const { currentBlockId, clearCurrentBlock, setCurrentBlockId } = usePanelEditorStore.getState()
|
||||
if (selectedIds.length === 1 && selectedIds[0] !== currentBlockId) {
|
||||
setCurrentBlockId(selectedIds[0])
|
||||
} else if (selectedIds.length === 0 && currentBlockId) {
|
||||
clearCurrentBlock()
|
||||
} else if (selectedIds.length > 1 && currentBlockId) {
|
||||
clearCurrentBlock()
|
||||
if (selectedIds.length === 0) {
|
||||
if (currentBlockId) clearCurrentBlock()
|
||||
} else {
|
||||
const lastSelectedId = selectedIds[selectedIds.length - 1]
|
||||
if (lastSelectedId !== currentBlockId) {
|
||||
setCurrentBlockId(lastSelectedId)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -246,7 +249,6 @@ const WorkflowContent = React.memo(
|
||||
const [selectedEdges, setSelectedEdges] = useState<SelectedEdgesMap>(new Map())
|
||||
const [isErrorConnectionDrag, setIsErrorConnectionDrag] = useState(false)
|
||||
const canvasContainerRef = useRef<HTMLDivElement>(null)
|
||||
const selectedIdsRef = useRef<string[] | null>(null)
|
||||
const embeddedFitFrameRef = useRef<number | null>(null)
|
||||
const hasCompletedInitialEmbeddedFitRef = useRef(false)
|
||||
const canvasMode = useCanvasModeStore((state) => state.mode)
|
||||
@@ -2477,6 +2479,16 @@ const WorkflowContent = React.memo(
|
||||
// Local state for nodes - allows smooth drag without store updates on every frame
|
||||
const [displayNodes, setDisplayNodes] = useState<Node[]>([])
|
||||
|
||||
const selectedNodeIds = useMemo(
|
||||
() => displayNodes.filter((node) => node.selected).map((node) => node.id),
|
||||
[displayNodes]
|
||||
)
|
||||
const selectedNodeIdsKey = selectedNodeIds.join(',')
|
||||
|
||||
useEffect(() => {
|
||||
syncPanelWithSelection(selectedNodeIds)
|
||||
}, [selectedNodeIdsKey])
|
||||
|
||||
useEffect(() => {
|
||||
// Check for pending selection (from paste/duplicate), otherwise preserve existing selection
|
||||
if (pendingSelection && pendingSelection.length > 0) {
|
||||
@@ -2488,10 +2500,8 @@ const WorkflowContent = React.memo(
|
||||
...node,
|
||||
selected: pendingSet.has(node.id),
|
||||
}))
|
||||
const resolved = resolveParentChildSelectionConflicts(withSelection, blocks)
|
||||
const resolved = resolveSelectionConflicts(withSelection, blocks)
|
||||
setDisplayNodes(resolved)
|
||||
const selectedIds = resolved.filter((node) => node.selected).map((node) => node.id)
|
||||
syncPanelWithSelection(selectedIds)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -2709,19 +2719,20 @@ const WorkflowContent = React.memo(
|
||||
/** Handles node changes - applies changes and resolves parent-child selection conflicts. */
|
||||
const onNodesChange = useCallback(
|
||||
(changes: NodeChange[]) => {
|
||||
selectedIdsRef.current = null
|
||||
setDisplayNodes((nds) => {
|
||||
const updated = applyNodeChanges(changes, nds)
|
||||
const hasSelectionChange = changes.some((c) => c.type === 'select')
|
||||
const hasSelectionChange = changes.some((c) => c.type === 'select')
|
||||
setDisplayNodes((currentNodes) => {
|
||||
const updated = applyNodeChanges(changes, currentNodes)
|
||||
if (!hasSelectionChange) return updated
|
||||
const resolved = resolveParentChildSelectionConflicts(updated, blocks)
|
||||
selectedIdsRef.current = resolved.filter((node) => node.selected).map((node) => node.id)
|
||||
return resolved
|
||||
|
||||
const preferredNodeId = [...changes]
|
||||
.reverse()
|
||||
.find(
|
||||
(change): change is NodeChange & { id: string; selected: boolean } =>
|
||||
change.type === 'select' && 'selected' in change && change.selected === true
|
||||
)?.id
|
||||
|
||||
return resolveSelectionConflicts(updated, blocks, preferredNodeId)
|
||||
})
|
||||
const selectedIds = selectedIdsRef.current as string[] | null
|
||||
if (selectedIds !== null) {
|
||||
syncPanelWithSelection(selectedIds)
|
||||
}
|
||||
|
||||
// Handle position changes (e.g., from keyboard arrow key movement)
|
||||
// Update container dimensions when child nodes are moved and persist to backend
|
||||
@@ -3160,7 +3171,10 @@ const WorkflowContent = React.memo(
|
||||
parentId: currentParentId,
|
||||
})
|
||||
|
||||
// Capture all selected nodes' positions for multi-node undo/redo
|
||||
// Capture all selected nodes' positions for multi-node undo/redo.
|
||||
// Also include the dragged node itself — during shift+click+drag, ReactFlow
|
||||
// may have toggled (deselected) the node before drag starts, so it might not
|
||||
// appear in the selected set yet.
|
||||
const allNodes = getNodes()
|
||||
const selectedNodes = allNodes.filter((n) => n.selected)
|
||||
multiNodeDragStartRef.current.clear()
|
||||
@@ -3174,6 +3188,33 @@ const WorkflowContent = React.memo(
|
||||
})
|
||||
}
|
||||
})
|
||||
if (!multiNodeDragStartRef.current.has(node.id)) {
|
||||
multiNodeDragStartRef.current.set(node.id, {
|
||||
x: node.position.x,
|
||||
y: node.position.y,
|
||||
parentId: currentParentId ?? undefined,
|
||||
})
|
||||
}
|
||||
|
||||
// When shift+clicking an already-selected node, ReactFlow toggles (deselects)
|
||||
// it via onNodesChange before drag starts. Re-select the dragged node so all
|
||||
// previously selected nodes move together as a group — but only if the
|
||||
// deselection wasn't from a parent-child conflict (e.g. dragging a child
|
||||
// when its parent subflow is selected).
|
||||
const draggedNodeInSelected = allNodes.find((n) => n.id === node.id)
|
||||
if (draggedNodeInSelected && !draggedNodeInSelected.selected && selectedNodes.length > 0) {
|
||||
const draggedParentId = blocks[node.id]?.data?.parentId
|
||||
const parentIsSelected =
|
||||
draggedParentId && selectedNodes.some((n) => n.id === draggedParentId)
|
||||
const contextMismatch =
|
||||
getNodeSelectionContextId(draggedNodeInSelected, blocks) !==
|
||||
getNodeSelectionContextId(selectedNodes[0], blocks)
|
||||
if (!parentIsSelected && !contextMismatch) {
|
||||
setDisplayNodes((currentNodes) =>
|
||||
currentNodes.map((n) => (n.id === node.id ? { ...n, selected: true } : n))
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
[blocks, setDragStartPosition, getNodes, setPotentialParentId]
|
||||
)
|
||||
@@ -3453,7 +3494,7 @@ const WorkflowContent = React.memo(
|
||||
})
|
||||
|
||||
// Apply visual deselection of children
|
||||
setDisplayNodes((allNodes) => resolveParentChildSelectionConflicts(allNodes, blocks))
|
||||
setDisplayNodes((allNodes) => resolveSelectionConflicts(allNodes, blocks))
|
||||
},
|
||||
[blocks]
|
||||
)
|
||||
@@ -3604,19 +3645,25 @@ const WorkflowContent = React.memo(
|
||||
|
||||
/**
|
||||
* Handles node click to select the node in ReactFlow.
|
||||
* Parent-child conflict resolution happens automatically in onNodesChange.
|
||||
* Uses the controlled display node state so parent-child conflicts are resolved
|
||||
* consistently for click, shift-click, and marquee selection.
|
||||
*/
|
||||
const handleNodeClick = useCallback(
|
||||
(event: React.MouseEvent, node: Node) => {
|
||||
const isMultiSelect = event.shiftKey || event.metaKey || event.ctrlKey
|
||||
setNodes((nodes) =>
|
||||
nodes.map((n) => ({
|
||||
...n,
|
||||
selected: isMultiSelect ? (n.id === node.id ? true : n.selected) : n.id === node.id,
|
||||
setDisplayNodes((currentNodes) => {
|
||||
const updated = currentNodes.map((currentNode) => ({
|
||||
...currentNode,
|
||||
selected: isMultiSelect
|
||||
? currentNode.id === node.id
|
||||
? true
|
||||
: currentNode.selected
|
||||
: currentNode.id === node.id,
|
||||
}))
|
||||
)
|
||||
return resolveSelectionConflicts(updated, blocks, isMultiSelect ? node.id : undefined)
|
||||
})
|
||||
},
|
||||
[setNodes]
|
||||
[blocks]
|
||||
)
|
||||
|
||||
/** Handles edge selection with container context tracking and Shift-click multi-selection. */
|
||||
@@ -3624,16 +3671,10 @@ const WorkflowContent = React.memo(
|
||||
(event: React.MouseEvent, edge: any) => {
|
||||
event.stopPropagation() // Prevent bubbling
|
||||
|
||||
// Determine if edge is inside a loop by checking its source/target nodes
|
||||
const sourceNode = getNodes().find((n) => n.id === edge.source)
|
||||
const targetNode = getNodes().find((n) => n.id === edge.target)
|
||||
|
||||
// An edge is inside a loop if either source or target has a parent
|
||||
// If source and target have different parents, prioritize source's parent
|
||||
const parentLoopId = sourceNode?.parentId || targetNode?.parentId
|
||||
|
||||
// Create a unique identifier that combines edge ID and parent context
|
||||
const contextId = `${edge.id}${parentLoopId ? `-${parentLoopId}` : ''}`
|
||||
const contextId = `${edge.id}${(() => {
|
||||
const selectionContextId = getEdgeSelectionContextId(edge, getNodes(), blocks)
|
||||
return selectionContextId ? `-${selectionContextId}` : ''
|
||||
})()}`
|
||||
|
||||
if (event.shiftKey) {
|
||||
// Shift-click: toggle edge in selection
|
||||
@@ -3651,7 +3692,7 @@ const WorkflowContent = React.memo(
|
||||
setSelectedEdges(new Map([[contextId, edge.id]]))
|
||||
}
|
||||
},
|
||||
[getNodes]
|
||||
[blocks, getNodes]
|
||||
)
|
||||
|
||||
/** Stable delete handler to avoid creating new function references per edge. */
|
||||
|
||||
@@ -7,7 +7,7 @@ export const AshbyBlock: BlockConfig = {
|
||||
name: 'Ashby',
|
||||
description: 'Manage candidates, jobs, and applications in Ashby',
|
||||
longDescription:
|
||||
'Integrate Ashby into the workflow. Can list, search, create, and update candidates, list and get job details, create notes, list notes, list and get applications, create applications, and list offers.',
|
||||
'Integrate Ashby into the workflow. Manage candidates (list, get, create, update, search, tag), applications (list, get, create, change stage), jobs (list, get), job postings (list, get), offers (list, get), notes (list, create), interviews (list), and reference data (sources, tags, archive reasons, custom fields, departments, locations, openings, users).',
|
||||
docsLink: 'https://docs.sim.ai/tools/ashby',
|
||||
category: 'tools',
|
||||
bgColor: '#5D4ED6',
|
||||
@@ -45,6 +45,21 @@ export const AshbyBlock: BlockConfig = {
|
||||
{ label: 'Get Application', id: 'get_application' },
|
||||
{ label: 'Create Application', id: 'create_application' },
|
||||
{ label: 'List Offers', id: 'list_offers' },
|
||||
{ label: 'Change Application Stage', id: 'change_application_stage' },
|
||||
{ label: 'Add Candidate Tag', id: 'add_candidate_tag' },
|
||||
{ label: 'Remove Candidate Tag', id: 'remove_candidate_tag' },
|
||||
{ label: 'Get Offer', id: 'get_offer' },
|
||||
{ label: 'List Sources', id: 'list_sources' },
|
||||
{ label: 'List Candidate Tags', id: 'list_candidate_tags' },
|
||||
{ label: 'List Archive Reasons', id: 'list_archive_reasons' },
|
||||
{ label: 'List Custom Fields', id: 'list_custom_fields' },
|
||||
{ label: 'List Departments', id: 'list_departments' },
|
||||
{ label: 'List Locations', id: 'list_locations' },
|
||||
{ label: 'List Job Postings', id: 'list_job_postings' },
|
||||
{ label: 'Get Job Posting', id: 'get_job_posting' },
|
||||
{ label: 'List Openings', id: 'list_openings' },
|
||||
{ label: 'List Users', id: 'list_users' },
|
||||
{ label: 'List Interviews', id: 'list_interviews' },
|
||||
],
|
||||
value: () => 'list_candidates',
|
||||
},
|
||||
@@ -56,24 +71,34 @@ export const AshbyBlock: BlockConfig = {
|
||||
placeholder: 'Enter your Ashby API key',
|
||||
password: true,
|
||||
},
|
||||
|
||||
// Get Candidate / Create Note / List Notes / Update Candidate - candidateId
|
||||
{
|
||||
id: 'candidateId',
|
||||
title: 'Candidate ID',
|
||||
type: 'short-input',
|
||||
required: {
|
||||
field: 'operation',
|
||||
value: ['get_candidate', 'create_note', 'list_notes', 'update_candidate'],
|
||||
value: [
|
||||
'get_candidate',
|
||||
'create_note',
|
||||
'list_notes',
|
||||
'update_candidate',
|
||||
'add_candidate_tag',
|
||||
'remove_candidate_tag',
|
||||
],
|
||||
},
|
||||
placeholder: 'Enter candidate UUID',
|
||||
condition: {
|
||||
field: 'operation',
|
||||
value: ['get_candidate', 'create_note', 'list_notes', 'update_candidate'],
|
||||
value: [
|
||||
'get_candidate',
|
||||
'create_note',
|
||||
'list_notes',
|
||||
'update_candidate',
|
||||
'add_candidate_tag',
|
||||
'remove_candidate_tag',
|
||||
],
|
||||
},
|
||||
},
|
||||
|
||||
// Create Candidate fields
|
||||
{
|
||||
id: 'name',
|
||||
title: 'Name',
|
||||
@@ -86,22 +111,10 @@ export const AshbyBlock: BlockConfig = {
|
||||
id: 'email',
|
||||
title: 'Email',
|
||||
type: 'short-input',
|
||||
required: { field: 'operation', value: 'create_candidate' },
|
||||
placeholder: 'Email address',
|
||||
condition: { field: 'operation', value: ['create_candidate', 'update_candidate'] },
|
||||
},
|
||||
{
|
||||
id: 'emailType',
|
||||
title: 'Email Type',
|
||||
type: 'dropdown',
|
||||
options: [
|
||||
{ label: 'Work', id: 'Work' },
|
||||
{ label: 'Personal', id: 'Personal' },
|
||||
{ label: 'Other', id: 'Other' },
|
||||
],
|
||||
value: () => 'Work',
|
||||
condition: { field: 'operation', value: ['create_candidate', 'update_candidate'] },
|
||||
mode: 'advanced',
|
||||
},
|
||||
{
|
||||
id: 'phoneNumber',
|
||||
title: 'Phone Number',
|
||||
@@ -110,19 +123,6 @@ export const AshbyBlock: BlockConfig = {
|
||||
condition: { field: 'operation', value: ['create_candidate', 'update_candidate'] },
|
||||
mode: 'advanced',
|
||||
},
|
||||
{
|
||||
id: 'phoneType',
|
||||
title: 'Phone Type',
|
||||
type: 'dropdown',
|
||||
options: [
|
||||
{ label: 'Work', id: 'Work' },
|
||||
{ label: 'Personal', id: 'Personal' },
|
||||
{ label: 'Other', id: 'Other' },
|
||||
],
|
||||
value: () => 'Work',
|
||||
condition: { field: 'operation', value: ['create_candidate', 'update_candidate'] },
|
||||
mode: 'advanced',
|
||||
},
|
||||
{
|
||||
id: 'linkedInUrl',
|
||||
title: 'LinkedIn URL',
|
||||
@@ -150,8 +150,6 @@ export const AshbyBlock: BlockConfig = {
|
||||
},
|
||||
mode: 'advanced',
|
||||
},
|
||||
|
||||
// Update Candidate fields
|
||||
{
|
||||
id: 'updateName',
|
||||
title: 'Name',
|
||||
@@ -168,8 +166,6 @@ export const AshbyBlock: BlockConfig = {
|
||||
condition: { field: 'operation', value: 'update_candidate' },
|
||||
mode: 'advanced',
|
||||
},
|
||||
|
||||
// Search Candidates fields
|
||||
{
|
||||
id: 'searchName',
|
||||
title: 'Name',
|
||||
@@ -184,8 +180,6 @@ export const AshbyBlock: BlockConfig = {
|
||||
placeholder: 'Search by candidate email',
|
||||
condition: { field: 'operation', value: 'search_candidates' },
|
||||
},
|
||||
|
||||
// Get Job fields
|
||||
{
|
||||
id: 'jobId',
|
||||
title: 'Job ID',
|
||||
@@ -194,18 +188,20 @@ export const AshbyBlock: BlockConfig = {
|
||||
placeholder: 'Enter job UUID',
|
||||
condition: { field: 'operation', value: ['get_job', 'create_application'] },
|
||||
},
|
||||
|
||||
// Get Application fields
|
||||
{
|
||||
id: 'applicationId',
|
||||
title: 'Application ID',
|
||||
type: 'short-input',
|
||||
required: { field: 'operation', value: 'get_application' },
|
||||
required: {
|
||||
field: 'operation',
|
||||
value: ['get_application', 'change_application_stage'],
|
||||
},
|
||||
placeholder: 'Enter application UUID',
|
||||
condition: { field: 'operation', value: 'get_application' },
|
||||
condition: {
|
||||
field: 'operation',
|
||||
value: ['get_application', 'change_application_stage', 'list_interviews'],
|
||||
},
|
||||
},
|
||||
|
||||
// Create Application fields
|
||||
{
|
||||
id: 'appCandidateId',
|
||||
title: 'Candidate ID',
|
||||
@@ -226,9 +222,12 @@ export const AshbyBlock: BlockConfig = {
|
||||
id: 'interviewStageId',
|
||||
title: 'Interview Stage ID',
|
||||
type: 'short-input',
|
||||
placeholder: 'Interview stage UUID (defaults to first Lead stage)',
|
||||
condition: { field: 'operation', value: 'create_application' },
|
||||
mode: 'advanced',
|
||||
required: { field: 'operation', value: 'change_application_stage' },
|
||||
placeholder: 'Interview stage UUID',
|
||||
condition: {
|
||||
field: 'operation',
|
||||
value: ['create_application', 'change_application_stage', 'list_interviews'],
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'creditedToUserId',
|
||||
@@ -257,8 +256,6 @@ Output only the ISO 8601 timestamp string, nothing else.`,
|
||||
generationType: 'timestamp',
|
||||
},
|
||||
},
|
||||
|
||||
// Create Note fields
|
||||
{
|
||||
id: 'note',
|
||||
title: 'Note',
|
||||
@@ -286,8 +283,6 @@ Output only the ISO 8601 timestamp string, nothing else.`,
|
||||
condition: { field: 'operation', value: 'create_note' },
|
||||
mode: 'advanced',
|
||||
},
|
||||
|
||||
// List Applications filter fields
|
||||
{
|
||||
id: 'filterStatus',
|
||||
title: 'Status Filter',
|
||||
@@ -338,8 +333,6 @@ Output only the ISO 8601 timestamp string, nothing else.`,
|
||||
generationType: 'timestamp',
|
||||
},
|
||||
},
|
||||
|
||||
// List Jobs status filter
|
||||
{
|
||||
id: 'jobStatus',
|
||||
title: 'Status Filter',
|
||||
@@ -355,8 +348,6 @@ Output only the ISO 8601 timestamp string, nothing else.`,
|
||||
condition: { field: 'operation', value: 'list_jobs' },
|
||||
mode: 'advanced',
|
||||
},
|
||||
|
||||
// Pagination fields for list operations
|
||||
{
|
||||
id: 'cursor',
|
||||
title: 'Cursor',
|
||||
@@ -364,7 +355,16 @@ Output only the ISO 8601 timestamp string, nothing else.`,
|
||||
placeholder: 'Pagination cursor from previous response',
|
||||
condition: {
|
||||
field: 'operation',
|
||||
value: ['list_candidates', 'list_jobs', 'list_applications', 'list_notes', 'list_offers'],
|
||||
value: [
|
||||
'list_candidates',
|
||||
'list_jobs',
|
||||
'list_applications',
|
||||
'list_notes',
|
||||
'list_offers',
|
||||
'list_openings',
|
||||
'list_users',
|
||||
'list_interviews',
|
||||
],
|
||||
},
|
||||
mode: 'advanced',
|
||||
},
|
||||
@@ -375,12 +375,57 @@ Output only the ISO 8601 timestamp string, nothing else.`,
|
||||
placeholder: 'Results per page (default 100)',
|
||||
condition: {
|
||||
field: 'operation',
|
||||
value: ['list_candidates', 'list_jobs', 'list_applications', 'list_notes', 'list_offers'],
|
||||
value: [
|
||||
'list_candidates',
|
||||
'list_jobs',
|
||||
'list_applications',
|
||||
'list_notes',
|
||||
'list_offers',
|
||||
'list_openings',
|
||||
'list_users',
|
||||
'list_interviews',
|
||||
],
|
||||
},
|
||||
mode: 'advanced',
|
||||
},
|
||||
|
||||
// Trigger subBlocks
|
||||
{
|
||||
id: 'tagId',
|
||||
title: 'Tag ID',
|
||||
type: 'short-input',
|
||||
required: {
|
||||
field: 'operation',
|
||||
value: ['add_candidate_tag', 'remove_candidate_tag'],
|
||||
},
|
||||
placeholder: 'Enter tag UUID',
|
||||
condition: {
|
||||
field: 'operation',
|
||||
value: ['add_candidate_tag', 'remove_candidate_tag'],
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'archiveReasonId',
|
||||
title: 'Archive Reason ID',
|
||||
type: 'short-input',
|
||||
placeholder: 'Archive reason UUID (required for Archived stages)',
|
||||
condition: { field: 'operation', value: 'change_application_stage' },
|
||||
mode: 'advanced',
|
||||
},
|
||||
{
|
||||
id: 'offerId',
|
||||
title: 'Offer ID',
|
||||
type: 'short-input',
|
||||
required: { field: 'operation', value: 'get_offer' },
|
||||
placeholder: 'Enter offer UUID',
|
||||
condition: { field: 'operation', value: 'get_offer' },
|
||||
},
|
||||
{
|
||||
id: 'jobPostingId',
|
||||
title: 'Job Posting ID',
|
||||
type: 'short-input',
|
||||
required: { field: 'operation', value: 'get_job_posting' },
|
||||
placeholder: 'Enter job posting UUID',
|
||||
condition: { field: 'operation', value: 'get_job_posting' },
|
||||
},
|
||||
...getTrigger('ashby_application_submit').subBlocks,
|
||||
...getTrigger('ashby_candidate_stage_change').subBlocks,
|
||||
...getTrigger('ashby_candidate_hire').subBlocks,
|
||||
@@ -391,17 +436,32 @@ Output only the ISO 8601 timestamp string, nothing else.`,
|
||||
|
||||
tools: {
|
||||
access: [
|
||||
'ashby_add_candidate_tag',
|
||||
'ashby_change_application_stage',
|
||||
'ashby_create_application',
|
||||
'ashby_create_candidate',
|
||||
'ashby_create_note',
|
||||
'ashby_get_application',
|
||||
'ashby_get_candidate',
|
||||
'ashby_get_job',
|
||||
'ashby_get_job_posting',
|
||||
'ashby_get_offer',
|
||||
'ashby_list_applications',
|
||||
'ashby_list_archive_reasons',
|
||||
'ashby_list_candidate_tags',
|
||||
'ashby_list_candidates',
|
||||
'ashby_list_custom_fields',
|
||||
'ashby_list_departments',
|
||||
'ashby_list_interviews',
|
||||
'ashby_list_job_postings',
|
||||
'ashby_list_jobs',
|
||||
'ashby_list_locations',
|
||||
'ashby_list_notes',
|
||||
'ashby_list_offers',
|
||||
'ashby_list_openings',
|
||||
'ashby_list_sources',
|
||||
'ashby_list_users',
|
||||
'ashby_remove_candidate_tag',
|
||||
'ashby_search_candidates',
|
||||
'ashby_update_candidate',
|
||||
],
|
||||
@@ -419,10 +479,8 @@ Output only the ISO 8601 timestamp string, nothing else.`,
|
||||
if (params.sendNotifications === 'true' || params.sendNotifications === true) {
|
||||
result.sendNotifications = true
|
||||
}
|
||||
// Create Application params
|
||||
if (params.appCandidateId) result.candidateId = params.appCandidateId
|
||||
if (params.appCreatedAt) result.createdAt = params.appCreatedAt
|
||||
// Update Candidate params
|
||||
if (params.updateName) result.name = params.updateName
|
||||
return result
|
||||
},
|
||||
@@ -435,9 +493,7 @@ Output only the ISO 8601 timestamp string, nothing else.`,
|
||||
candidateId: { type: 'string', description: 'Candidate UUID' },
|
||||
name: { type: 'string', description: 'Candidate full name' },
|
||||
email: { type: 'string', description: 'Email address' },
|
||||
emailType: { type: 'string', description: 'Email type (Personal, Work, Other)' },
|
||||
phoneNumber: { type: 'string', description: 'Phone number' },
|
||||
phoneType: { type: 'string', description: 'Phone type (Personal, Work, Other)' },
|
||||
linkedInUrl: { type: 'string', description: 'LinkedIn profile URL' },
|
||||
githubUrl: { type: 'string', description: 'GitHub profile URL' },
|
||||
websiteUrl: { type: 'string', description: 'Personal website URL' },
|
||||
@@ -462,6 +518,10 @@ Output only the ISO 8601 timestamp string, nothing else.`,
|
||||
jobStatus: { type: 'string', description: 'Job status filter' },
|
||||
cursor: { type: 'string', description: 'Pagination cursor' },
|
||||
perPage: { type: 'number', description: 'Results per page' },
|
||||
tagId: { type: 'string', description: 'Tag UUID' },
|
||||
offerId: { type: 'string', description: 'Offer UUID' },
|
||||
jobPostingId: { type: 'string', description: 'Job posting UUID' },
|
||||
archiveReasonId: { type: 'string', description: 'Archive reason UUID' },
|
||||
},
|
||||
|
||||
outputs: {
|
||||
@@ -486,12 +546,73 @@ Output only the ISO 8601 timestamp string, nothing else.`,
|
||||
},
|
||||
offers: {
|
||||
type: 'json',
|
||||
description: 'List of offers (id, status, candidate, job, createdAt, updatedAt)',
|
||||
description:
|
||||
'List of offers (id, offerStatus, acceptanceStatus, applicationId, startDate, salary, openingId)',
|
||||
},
|
||||
archiveReasons: {
|
||||
type: 'json',
|
||||
description: 'List of archive reasons (id, text, reasonType, isArchived)',
|
||||
},
|
||||
sources: {
|
||||
type: 'json',
|
||||
description: 'List of sources (id, title, isArchived)',
|
||||
},
|
||||
customFields: {
|
||||
type: 'json',
|
||||
description: 'List of custom fields (id, title, fieldType, objectType, isArchived)',
|
||||
},
|
||||
departments: {
|
||||
type: 'json',
|
||||
description: 'List of departments (id, name, isArchived, parentId)',
|
||||
},
|
||||
locations: {
|
||||
type: 'json',
|
||||
description: 'List of locations (id, name, isArchived, isRemote, address)',
|
||||
},
|
||||
jobPostings: {
|
||||
type: 'json',
|
||||
description:
|
||||
'List of job postings (id, title, jobId, locationName, departmentName, employmentType, isListed, publishedDate)',
|
||||
},
|
||||
openings: {
|
||||
type: 'json',
|
||||
description: 'List of openings (id, openingState, isArchived, openedAt, closedAt)',
|
||||
},
|
||||
users: {
|
||||
type: 'json',
|
||||
description: 'List of users (id, firstName, lastName, email, isEnabled, globalRole)',
|
||||
},
|
||||
interviewSchedules: {
|
||||
type: 'json',
|
||||
description:
|
||||
'List of interview schedules (id, applicationId, interviewStageId, status, createdAt)',
|
||||
},
|
||||
tags: {
|
||||
type: 'json',
|
||||
description: 'List of candidate tags (id, title, isArchived)',
|
||||
},
|
||||
stageId: { type: 'string', description: 'Interview stage UUID after stage change' },
|
||||
success: { type: 'boolean', description: 'Whether the operation succeeded' },
|
||||
offerStatus: {
|
||||
type: 'string',
|
||||
description: 'Offer status (e.g. WaitingOnCandidateResponse, CandidateAccepted)',
|
||||
},
|
||||
acceptanceStatus: {
|
||||
type: 'string',
|
||||
description: 'Acceptance status (e.g. Accepted, Declined, Pending)',
|
||||
},
|
||||
applicationId: { type: 'string', description: 'Associated application UUID' },
|
||||
openingId: { type: 'string', description: 'Opening UUID associated with the offer' },
|
||||
salary: {
|
||||
type: 'json',
|
||||
description: 'Salary details from latest version (currencyCode, value)',
|
||||
},
|
||||
startDate: { type: 'string', description: 'Offer start date from latest version' },
|
||||
id: { type: 'string', description: 'Resource UUID' },
|
||||
name: { type: 'string', description: 'Resource name' },
|
||||
title: { type: 'string', description: 'Job title' },
|
||||
status: { type: 'string', description: 'Status' },
|
||||
noteId: { type: 'string', description: 'Created note UUID' },
|
||||
content: { type: 'string', description: 'Note content' },
|
||||
moreDataAvailable: { type: 'boolean', description: 'Whether more pages exist' },
|
||||
nextCursor: { type: 'string', description: 'Pagination cursor for next page' },
|
||||
|
||||
599
apps/sim/blocks/blocks/box.ts
Normal file
599
apps/sim/blocks/blocks/box.ts
Normal file
@@ -0,0 +1,599 @@
|
||||
import { BoxCompanyIcon } from '@/components/icons'
|
||||
import { getScopesForService } from '@/lib/oauth/utils'
|
||||
import type { BlockConfig } from '@/blocks/types'
|
||||
import { AuthMode } from '@/blocks/types'
|
||||
import { normalizeFileInput } from '@/blocks/utils'
|
||||
|
||||
export const BoxBlock: BlockConfig = {
|
||||
type: 'box',
|
||||
name: 'Box',
|
||||
description: 'Manage files, folders, and e-signatures with Box',
|
||||
longDescription:
|
||||
'Integrate Box into your workflow to manage files, folders, and e-signatures. Upload and download files, search content, create folders, send documents for e-signature, track signing status, and more.',
|
||||
docsLink: 'https://docs.sim.ai/tools/box',
|
||||
category: 'tools',
|
||||
bgColor: '#FFFFFF',
|
||||
icon: BoxCompanyIcon,
|
||||
authMode: AuthMode.OAuth,
|
||||
|
||||
subBlocks: [
|
||||
{
|
||||
id: 'operation',
|
||||
title: 'Operation',
|
||||
type: 'dropdown',
|
||||
options: [
|
||||
{ label: 'Upload File', id: 'upload_file' },
|
||||
{ label: 'Download File', id: 'download_file' },
|
||||
{ label: 'Get File Info', id: 'get_file_info' },
|
||||
{ label: 'List Folder Items', id: 'list_folder_items' },
|
||||
{ label: 'Create Folder', id: 'create_folder' },
|
||||
{ label: 'Delete File', id: 'delete_file' },
|
||||
{ label: 'Delete Folder', id: 'delete_folder' },
|
||||
{ label: 'Copy File', id: 'copy_file' },
|
||||
{ label: 'Search', id: 'search' },
|
||||
{ label: 'Update File', id: 'update_file' },
|
||||
{ label: 'Create Sign Request', id: 'sign_create_request' },
|
||||
{ label: 'Get Sign Request', id: 'sign_get_request' },
|
||||
{ label: 'List Sign Requests', id: 'sign_list_requests' },
|
||||
{ label: 'Cancel Sign Request', id: 'sign_cancel_request' },
|
||||
{ label: 'Resend Sign Request', id: 'sign_resend_request' },
|
||||
],
|
||||
value: () => 'upload_file',
|
||||
},
|
||||
{
|
||||
id: 'credential',
|
||||
title: 'Box Account',
|
||||
type: 'oauth-input',
|
||||
serviceId: 'box',
|
||||
requiredScopes: getScopesForService('box'),
|
||||
placeholder: 'Select Box account',
|
||||
required: true,
|
||||
},
|
||||
|
||||
// Upload File fields
|
||||
{
|
||||
id: 'uploadFile',
|
||||
title: 'File',
|
||||
type: 'file-upload',
|
||||
canonicalParamId: 'file',
|
||||
placeholder: 'Upload file to send to Box',
|
||||
mode: 'basic',
|
||||
multiple: false,
|
||||
required: { field: 'operation', value: 'upload_file' },
|
||||
condition: { field: 'operation', value: 'upload_file' },
|
||||
},
|
||||
{
|
||||
id: 'fileRef',
|
||||
title: 'File',
|
||||
type: 'short-input',
|
||||
canonicalParamId: 'file',
|
||||
placeholder: 'Reference file from previous blocks',
|
||||
mode: 'advanced',
|
||||
required: { field: 'operation', value: 'upload_file' },
|
||||
condition: { field: 'operation', value: 'upload_file' },
|
||||
},
|
||||
{
|
||||
id: 'parentFolderId',
|
||||
title: 'Parent Folder ID',
|
||||
type: 'short-input',
|
||||
placeholder: 'Folder ID (use "0" for root)',
|
||||
required: { field: 'operation', value: ['upload_file', 'create_folder', 'copy_file'] },
|
||||
condition: { field: 'operation', value: ['upload_file', 'create_folder', 'copy_file'] },
|
||||
},
|
||||
{
|
||||
id: 'uploadFileName',
|
||||
title: 'File Name',
|
||||
type: 'short-input',
|
||||
placeholder: 'Optional filename override',
|
||||
condition: { field: 'operation', value: 'upload_file' },
|
||||
mode: 'advanced',
|
||||
},
|
||||
|
||||
// File ID field (shared by download, get info, delete, copy, update)
|
||||
{
|
||||
id: 'fileId',
|
||||
title: 'File ID',
|
||||
type: 'short-input',
|
||||
placeholder: 'Box file ID',
|
||||
required: {
|
||||
field: 'operation',
|
||||
value: ['download_file', 'get_file_info', 'delete_file', 'copy_file', 'update_file'],
|
||||
},
|
||||
condition: {
|
||||
field: 'operation',
|
||||
value: ['download_file', 'get_file_info', 'delete_file', 'copy_file', 'update_file'],
|
||||
},
|
||||
},
|
||||
|
||||
// Folder ID field (shared by list, delete folder)
|
||||
{
|
||||
id: 'folderId',
|
||||
title: 'Folder ID',
|
||||
type: 'short-input',
|
||||
placeholder: 'Box folder ID (use "0" for root)',
|
||||
required: { field: 'operation', value: ['list_folder_items', 'delete_folder'] },
|
||||
condition: { field: 'operation', value: ['list_folder_items', 'delete_folder'] },
|
||||
},
|
||||
|
||||
// Create Folder fields
|
||||
{
|
||||
id: 'folderName',
|
||||
title: 'Folder Name',
|
||||
type: 'short-input',
|
||||
placeholder: 'Name for the new folder',
|
||||
required: { field: 'operation', value: 'create_folder' },
|
||||
condition: { field: 'operation', value: 'create_folder' },
|
||||
},
|
||||
|
||||
// Copy File fields
|
||||
{
|
||||
id: 'copyName',
|
||||
title: 'New Name',
|
||||
type: 'short-input',
|
||||
placeholder: 'Optional name for the copy',
|
||||
condition: { field: 'operation', value: 'copy_file' },
|
||||
},
|
||||
|
||||
// Search fields
|
||||
{
|
||||
id: 'query',
|
||||
title: 'Search Query',
|
||||
type: 'short-input',
|
||||
placeholder: 'Search query string',
|
||||
required: { field: 'operation', value: 'search' },
|
||||
condition: { field: 'operation', value: 'search' },
|
||||
},
|
||||
{
|
||||
id: 'ancestorFolderId',
|
||||
title: 'Ancestor Folder ID',
|
||||
type: 'short-input',
|
||||
placeholder: 'Restrict search to a folder',
|
||||
condition: { field: 'operation', value: 'search' },
|
||||
mode: 'advanced',
|
||||
},
|
||||
{
|
||||
id: 'fileExtensions',
|
||||
title: 'File Extensions',
|
||||
type: 'short-input',
|
||||
placeholder: 'e.g., pdf,docx,xlsx',
|
||||
condition: { field: 'operation', value: 'search' },
|
||||
mode: 'advanced',
|
||||
},
|
||||
{
|
||||
id: 'contentType',
|
||||
title: 'Content Type',
|
||||
type: 'dropdown',
|
||||
options: [
|
||||
{ label: 'All', id: '' },
|
||||
{ label: 'Files', id: 'file' },
|
||||
{ label: 'Folders', id: 'folder' },
|
||||
{ label: 'Web Links', id: 'web_link' },
|
||||
],
|
||||
value: () => '',
|
||||
condition: { field: 'operation', value: 'search' },
|
||||
mode: 'advanced',
|
||||
},
|
||||
|
||||
// Update File fields
|
||||
{
|
||||
id: 'newName',
|
||||
title: 'New Name',
|
||||
type: 'short-input',
|
||||
placeholder: 'Rename the file',
|
||||
condition: { field: 'operation', value: 'update_file' },
|
||||
},
|
||||
{
|
||||
id: 'description',
|
||||
title: 'Description',
|
||||
type: 'short-input',
|
||||
placeholder: 'File description (max 256 chars)',
|
||||
condition: { field: 'operation', value: 'update_file' },
|
||||
},
|
||||
{
|
||||
id: 'moveToFolderId',
|
||||
title: 'Move to Folder ID',
|
||||
type: 'short-input',
|
||||
placeholder: 'Move file to this folder',
|
||||
condition: { field: 'operation', value: 'update_file' },
|
||||
mode: 'advanced',
|
||||
},
|
||||
{
|
||||
id: 'tags',
|
||||
title: 'Tags',
|
||||
type: 'short-input',
|
||||
placeholder: 'Comma-separated tags',
|
||||
condition: { field: 'operation', value: 'update_file' },
|
||||
mode: 'advanced',
|
||||
},
|
||||
|
||||
// Delete Folder options
|
||||
{
|
||||
id: 'recursive',
|
||||
title: 'Delete Recursively',
|
||||
type: 'switch',
|
||||
condition: { field: 'operation', value: 'delete_folder' },
|
||||
},
|
||||
|
||||
// Shared pagination fields (file operations)
|
||||
{
|
||||
id: 'limit',
|
||||
title: 'Limit',
|
||||
type: 'short-input',
|
||||
placeholder: 'Max results per page',
|
||||
condition: {
|
||||
field: 'operation',
|
||||
value: ['list_folder_items', 'search', 'sign_list_requests'],
|
||||
},
|
||||
mode: 'advanced',
|
||||
},
|
||||
{
|
||||
id: 'offset',
|
||||
title: 'Offset',
|
||||
type: 'short-input',
|
||||
placeholder: 'Pagination offset',
|
||||
condition: { field: 'operation', value: ['list_folder_items', 'search'] },
|
||||
mode: 'advanced',
|
||||
},
|
||||
|
||||
// List Folder sort options
|
||||
{
|
||||
id: 'sort',
|
||||
title: 'Sort By',
|
||||
type: 'dropdown',
|
||||
options: [
|
||||
{ label: 'Default', id: '' },
|
||||
{ label: 'ID', id: 'id' },
|
||||
{ label: 'Name', id: 'name' },
|
||||
{ label: 'Date', id: 'date' },
|
||||
{ label: 'Size', id: 'size' },
|
||||
],
|
||||
value: () => '',
|
||||
condition: { field: 'operation', value: 'list_folder_items' },
|
||||
mode: 'advanced',
|
||||
},
|
||||
{
|
||||
id: 'direction',
|
||||
title: 'Sort Direction',
|
||||
type: 'dropdown',
|
||||
options: [
|
||||
{ label: 'Ascending', id: 'ASC' },
|
||||
{ label: 'Descending', id: 'DESC' },
|
||||
],
|
||||
value: () => 'ASC',
|
||||
condition: { field: 'operation', value: 'list_folder_items' },
|
||||
mode: 'advanced',
|
||||
},
|
||||
|
||||
// Sign Request fields
|
||||
{
|
||||
id: 'sourceFileIds',
|
||||
title: 'Source File IDs',
|
||||
type: 'short-input',
|
||||
placeholder: 'Comma-separated Box file IDs (e.g., 12345,67890)',
|
||||
required: { field: 'operation', value: 'sign_create_request' },
|
||||
condition: { field: 'operation', value: 'sign_create_request' },
|
||||
},
|
||||
{
|
||||
id: 'signerEmail',
|
||||
title: 'Signer Email',
|
||||
type: 'short-input',
|
||||
placeholder: 'Primary signer email address',
|
||||
required: { field: 'operation', value: 'sign_create_request' },
|
||||
condition: { field: 'operation', value: 'sign_create_request' },
|
||||
},
|
||||
{
|
||||
id: 'signerRole',
|
||||
title: 'Signer Role',
|
||||
type: 'dropdown',
|
||||
options: [
|
||||
{ label: 'Signer', id: 'signer' },
|
||||
{ label: 'Approver', id: 'approver' },
|
||||
{ label: 'Final Copy Reader', id: 'final_copy_reader' },
|
||||
],
|
||||
value: () => 'signer',
|
||||
condition: { field: 'operation', value: 'sign_create_request' },
|
||||
},
|
||||
{
|
||||
id: 'emailSubject',
|
||||
title: 'Email Subject',
|
||||
type: 'short-input',
|
||||
placeholder: 'Custom email subject line',
|
||||
condition: { field: 'operation', value: 'sign_create_request' },
|
||||
},
|
||||
{
|
||||
id: 'emailMessage',
|
||||
title: 'Email Message',
|
||||
type: 'long-input',
|
||||
placeholder: 'Custom message in the signing email',
|
||||
condition: { field: 'operation', value: 'sign_create_request' },
|
||||
},
|
||||
{
|
||||
id: 'signRequestName',
|
||||
title: 'Request Name',
|
||||
type: 'short-input',
|
||||
placeholder: 'Name for this sign request',
|
||||
condition: { field: 'operation', value: 'sign_create_request' },
|
||||
},
|
||||
{
|
||||
id: 'additionalSigners',
|
||||
title: 'Additional Signers',
|
||||
type: 'long-input',
|
||||
placeholder: '[{"email":"user@example.com","role":"signer"}]',
|
||||
condition: { field: 'operation', value: 'sign_create_request' },
|
||||
mode: 'advanced',
|
||||
},
|
||||
{
|
||||
id: 'signParentFolderId',
|
||||
title: 'Destination Folder ID',
|
||||
type: 'short-input',
|
||||
placeholder: 'Box folder ID for signed documents',
|
||||
condition: { field: 'operation', value: 'sign_create_request' },
|
||||
mode: 'advanced',
|
||||
},
|
||||
{
|
||||
id: 'daysValid',
|
||||
title: 'Days Valid',
|
||||
type: 'short-input',
|
||||
placeholder: 'Number of days before expiry (0-730)',
|
||||
condition: { field: 'operation', value: 'sign_create_request' },
|
||||
mode: 'advanced',
|
||||
},
|
||||
{
|
||||
id: 'areRemindersEnabled',
|
||||
title: 'Enable Reminders',
|
||||
type: 'switch',
|
||||
condition: { field: 'operation', value: 'sign_create_request' },
|
||||
mode: 'advanced',
|
||||
},
|
||||
{
|
||||
id: 'areTextSignaturesEnabled',
|
||||
title: 'Allow Text Signatures',
|
||||
type: 'switch',
|
||||
condition: { field: 'operation', value: 'sign_create_request' },
|
||||
mode: 'advanced',
|
||||
},
|
||||
{
|
||||
id: 'signatureColor',
|
||||
title: 'Signature Color',
|
||||
type: 'dropdown',
|
||||
options: [
|
||||
{ label: 'Blue', id: 'blue' },
|
||||
{ label: 'Black', id: 'black' },
|
||||
{ label: 'Red', id: 'red' },
|
||||
],
|
||||
value: () => 'blue',
|
||||
condition: { field: 'operation', value: 'sign_create_request' },
|
||||
mode: 'advanced',
|
||||
},
|
||||
{
|
||||
id: 'redirectUrl',
|
||||
title: 'Redirect URL',
|
||||
type: 'short-input',
|
||||
placeholder: 'URL to redirect after signing',
|
||||
condition: { field: 'operation', value: 'sign_create_request' },
|
||||
mode: 'advanced',
|
||||
},
|
||||
{
|
||||
id: 'declinedRedirectUrl',
|
||||
title: 'Declined Redirect URL',
|
||||
type: 'short-input',
|
||||
placeholder: 'URL to redirect after declining',
|
||||
condition: { field: 'operation', value: 'sign_create_request' },
|
||||
mode: 'advanced',
|
||||
},
|
||||
{
|
||||
id: 'isDocumentPreparationNeeded',
|
||||
title: 'Document Preparation Needed',
|
||||
type: 'switch',
|
||||
condition: { field: 'operation', value: 'sign_create_request' },
|
||||
mode: 'advanced',
|
||||
},
|
||||
{
|
||||
id: 'externalId',
|
||||
title: 'External ID',
|
||||
type: 'short-input',
|
||||
placeholder: 'External system reference ID',
|
||||
condition: { field: 'operation', value: 'sign_create_request' },
|
||||
mode: 'advanced',
|
||||
},
|
||||
|
||||
// Sign Request ID (shared by get, cancel, resend)
|
||||
{
|
||||
id: 'signRequestId',
|
||||
title: 'Sign Request ID',
|
||||
type: 'short-input',
|
||||
placeholder: 'Box Sign request ID',
|
||||
required: {
|
||||
field: 'operation',
|
||||
value: ['sign_get_request', 'sign_cancel_request', 'sign_resend_request'],
|
||||
},
|
||||
condition: {
|
||||
field: 'operation',
|
||||
value: ['sign_get_request', 'sign_cancel_request', 'sign_resend_request'],
|
||||
},
|
||||
},
|
||||
|
||||
// Sign list pagination marker
|
||||
{
|
||||
id: 'marker',
|
||||
title: 'Pagination Marker',
|
||||
type: 'short-input',
|
||||
placeholder: 'Marker from previous response',
|
||||
condition: { field: 'operation', value: 'sign_list_requests' },
|
||||
mode: 'advanced',
|
||||
},
|
||||
],
|
||||
|
||||
tools: {
|
||||
access: [
|
||||
'box_upload_file',
|
||||
'box_download_file',
|
||||
'box_get_file_info',
|
||||
'box_list_folder_items',
|
||||
'box_create_folder',
|
||||
'box_delete_file',
|
||||
'box_delete_folder',
|
||||
'box_copy_file',
|
||||
'box_search',
|
||||
'box_update_file',
|
||||
'box_sign_create_request',
|
||||
'box_sign_get_request',
|
||||
'box_sign_list_requests',
|
||||
'box_sign_cancel_request',
|
||||
'box_sign_resend_request',
|
||||
],
|
||||
config: {
|
||||
tool: (params) => {
|
||||
const op = params.operation as string
|
||||
if (op.startsWith('sign_')) {
|
||||
return `box_${op}`
|
||||
}
|
||||
return `box_${op}`
|
||||
},
|
||||
params: (params) => {
|
||||
const normalizedFile = normalizeFileInput(params.file, { single: true })
|
||||
if (normalizedFile) {
|
||||
params.file = normalizedFile
|
||||
}
|
||||
const { credential, operation, ...rest } = params
|
||||
|
||||
const baseParams: Record<string, unknown> = {
|
||||
accessToken: credential,
|
||||
}
|
||||
|
||||
switch (operation) {
|
||||
case 'upload_file':
|
||||
baseParams.parentFolderId = rest.parentFolderId
|
||||
baseParams.file = rest.file
|
||||
if (rest.uploadFileName) baseParams.fileName = rest.uploadFileName
|
||||
break
|
||||
case 'download_file':
|
||||
case 'get_file_info':
|
||||
case 'delete_file':
|
||||
baseParams.fileId = rest.fileId
|
||||
break
|
||||
case 'list_folder_items':
|
||||
baseParams.folderId = rest.folderId
|
||||
if (rest.limit) baseParams.limit = Number(rest.limit)
|
||||
if (rest.offset) baseParams.offset = Number(rest.offset)
|
||||
if (rest.sort) baseParams.sort = rest.sort
|
||||
if (rest.direction) baseParams.direction = rest.direction
|
||||
break
|
||||
case 'create_folder':
|
||||
baseParams.name = rest.folderName
|
||||
baseParams.parentFolderId = rest.parentFolderId
|
||||
break
|
||||
case 'delete_folder':
|
||||
baseParams.folderId = rest.folderId
|
||||
if (rest.recursive !== undefined) baseParams.recursive = rest.recursive
|
||||
break
|
||||
case 'copy_file':
|
||||
baseParams.fileId = rest.fileId
|
||||
baseParams.parentFolderId = rest.parentFolderId
|
||||
if (rest.copyName) baseParams.name = rest.copyName
|
||||
break
|
||||
case 'search':
|
||||
baseParams.query = rest.query
|
||||
if (rest.limit) baseParams.limit = Number(rest.limit)
|
||||
if (rest.offset) baseParams.offset = Number(rest.offset)
|
||||
if (rest.ancestorFolderId) baseParams.ancestorFolderId = rest.ancestorFolderId
|
||||
if (rest.fileExtensions) baseParams.fileExtensions = rest.fileExtensions
|
||||
if (rest.contentType) baseParams.type = rest.contentType
|
||||
break
|
||||
case 'update_file':
|
||||
baseParams.fileId = rest.fileId
|
||||
if (rest.newName) baseParams.name = rest.newName
|
||||
if (rest.description !== undefined) baseParams.description = rest.description
|
||||
if (rest.moveToFolderId) baseParams.parentFolderId = rest.moveToFolderId
|
||||
if (rest.tags) baseParams.tags = rest.tags
|
||||
break
|
||||
case 'sign_create_request':
|
||||
baseParams.sourceFileIds = rest.sourceFileIds
|
||||
baseParams.signerEmail = rest.signerEmail
|
||||
if (rest.signerRole) baseParams.signerRole = rest.signerRole
|
||||
if (rest.additionalSigners) baseParams.additionalSigners = rest.additionalSigners
|
||||
if (rest.signParentFolderId) baseParams.parentFolderId = rest.signParentFolderId
|
||||
if (rest.emailSubject) baseParams.emailSubject = rest.emailSubject
|
||||
if (rest.emailMessage) baseParams.emailMessage = rest.emailMessage
|
||||
if (rest.signRequestName) baseParams.name = rest.signRequestName
|
||||
if (rest.daysValid) baseParams.daysValid = Number(rest.daysValid)
|
||||
if (rest.areRemindersEnabled !== undefined)
|
||||
baseParams.areRemindersEnabled = rest.areRemindersEnabled
|
||||
if (rest.areTextSignaturesEnabled !== undefined)
|
||||
baseParams.areTextSignaturesEnabled = rest.areTextSignaturesEnabled
|
||||
if (rest.signatureColor) baseParams.signatureColor = rest.signatureColor
|
||||
if (rest.redirectUrl) baseParams.redirectUrl = rest.redirectUrl
|
||||
if (rest.declinedRedirectUrl) baseParams.declinedRedirectUrl = rest.declinedRedirectUrl
|
||||
if (rest.isDocumentPreparationNeeded !== undefined)
|
||||
baseParams.isDocumentPreparationNeeded = rest.isDocumentPreparationNeeded
|
||||
if (rest.externalId) baseParams.externalId = rest.externalId
|
||||
break
|
||||
case 'sign_get_request':
|
||||
case 'sign_cancel_request':
|
||||
case 'sign_resend_request':
|
||||
baseParams.signRequestId = rest.signRequestId
|
||||
break
|
||||
case 'sign_list_requests':
|
||||
if (rest.limit) baseParams.limit = Number(rest.limit)
|
||||
if (rest.marker) baseParams.marker = rest.marker
|
||||
break
|
||||
}
|
||||
|
||||
return baseParams
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
inputs: {
|
||||
operation: { type: 'string', description: 'Operation to perform' },
|
||||
credential: { type: 'string', description: 'Box OAuth credential' },
|
||||
file: { type: 'json', description: 'File to upload (canonical param)' },
|
||||
fileId: { type: 'string', description: 'Box file ID' },
|
||||
folderId: { type: 'string', description: 'Box folder ID' },
|
||||
parentFolderId: { type: 'string', description: 'Parent folder ID' },
|
||||
query: { type: 'string', description: 'Search query' },
|
||||
sourceFileIds: { type: 'string', description: 'Comma-separated Box file IDs' },
|
||||
signerEmail: { type: 'string', description: 'Primary signer email address' },
|
||||
signRequestId: { type: 'string', description: 'Sign request ID' },
|
||||
},
|
||||
|
||||
outputs: {
|
||||
id: 'string',
|
||||
name: 'string',
|
||||
description: 'string',
|
||||
size: 'number',
|
||||
sha1: 'string',
|
||||
createdAt: 'string',
|
||||
modifiedAt: 'string',
|
||||
createdBy: 'json',
|
||||
modifiedBy: 'json',
|
||||
ownedBy: 'json',
|
||||
parentId: 'string',
|
||||
parentName: 'string',
|
||||
sharedLink: 'json',
|
||||
tags: 'json',
|
||||
commentCount: 'number',
|
||||
file: 'file',
|
||||
content: 'string',
|
||||
entries: 'json',
|
||||
totalCount: 'number',
|
||||
offset: 'number',
|
||||
limit: 'number',
|
||||
results: 'json',
|
||||
deleted: 'boolean',
|
||||
message: 'string',
|
||||
status: 'string',
|
||||
shortId: 'string',
|
||||
signers: 'json',
|
||||
sourceFiles: 'json',
|
||||
emailSubject: 'string',
|
||||
emailMessage: 'string',
|
||||
daysValid: 'number',
|
||||
autoExpireAt: 'string',
|
||||
prepareUrl: 'string',
|
||||
senderEmail: 'string',
|
||||
signRequests: 'json',
|
||||
count: 'number',
|
||||
nextMarker: 'string',
|
||||
},
|
||||
}
|
||||
372
apps/sim/blocks/blocks/docusign.ts
Normal file
372
apps/sim/blocks/blocks/docusign.ts
Normal file
@@ -0,0 +1,372 @@
|
||||
import { DocuSignIcon } from '@/components/icons'
|
||||
import { getScopesForService } from '@/lib/oauth/utils'
|
||||
import type { BlockConfig } from '@/blocks/types'
|
||||
import { AuthMode } from '@/blocks/types'
|
||||
import { normalizeFileInput } from '@/blocks/utils'
|
||||
import type { DocuSignResponse } from '@/tools/docusign/types'
|
||||
|
||||
export const DocuSignBlock: BlockConfig<DocuSignResponse> = {
|
||||
type: 'docusign',
|
||||
name: 'DocuSign',
|
||||
description: 'Send documents for e-signature via DocuSign',
|
||||
longDescription:
|
||||
'Create and send envelopes for e-signature, use templates, check signing status, download signed documents, and manage recipients with DocuSign.',
|
||||
docsLink: 'https://docs.sim.ai/tools/docusign',
|
||||
category: 'tools',
|
||||
bgColor: '#FFFFFF',
|
||||
icon: DocuSignIcon,
|
||||
authMode: AuthMode.OAuth,
|
||||
|
||||
subBlocks: [
|
||||
{
|
||||
id: 'operation',
|
||||
title: 'Operation',
|
||||
type: 'dropdown',
|
||||
options: [
|
||||
{ label: 'Send Envelope', id: 'send_envelope' },
|
||||
{ label: 'Send from Template', id: 'create_from_template' },
|
||||
{ label: 'Get Envelope', id: 'get_envelope' },
|
||||
{ label: 'List Envelopes', id: 'list_envelopes' },
|
||||
{ label: 'Void Envelope', id: 'void_envelope' },
|
||||
{ label: 'Download Document', id: 'download_document' },
|
||||
{ label: 'List Templates', id: 'list_templates' },
|
||||
{ label: 'List Recipients', id: 'list_recipients' },
|
||||
],
|
||||
value: () => 'send_envelope',
|
||||
},
|
||||
{
|
||||
id: 'credential',
|
||||
title: 'DocuSign Account',
|
||||
type: 'oauth-input',
|
||||
canonicalParamId: 'oauthCredential',
|
||||
mode: 'basic',
|
||||
serviceId: 'docusign',
|
||||
requiredScopes: getScopesForService('docusign'),
|
||||
placeholder: 'Select DocuSign account',
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
id: 'manualCredential',
|
||||
title: 'DocuSign Account',
|
||||
type: 'short-input',
|
||||
canonicalParamId: 'oauthCredential',
|
||||
mode: 'advanced',
|
||||
placeholder: 'Enter credential ID',
|
||||
required: true,
|
||||
},
|
||||
|
||||
// Send Envelope fields
|
||||
{
|
||||
id: 'emailSubject',
|
||||
title: 'Email Subject',
|
||||
type: 'short-input',
|
||||
placeholder: 'Please sign this document',
|
||||
condition: { field: 'operation', value: ['send_envelope', 'create_from_template'] },
|
||||
required: { field: 'operation', value: 'send_envelope' },
|
||||
},
|
||||
{
|
||||
id: 'emailBody',
|
||||
title: 'Email Body',
|
||||
type: 'long-input',
|
||||
placeholder: 'Optional message to include in the email',
|
||||
condition: { field: 'operation', value: ['send_envelope', 'create_from_template'] },
|
||||
mode: 'advanced',
|
||||
},
|
||||
{
|
||||
id: 'signerEmail',
|
||||
title: 'Signer Email',
|
||||
type: 'short-input',
|
||||
placeholder: 'signer@example.com',
|
||||
condition: { field: 'operation', value: 'send_envelope' },
|
||||
required: { field: 'operation', value: 'send_envelope' },
|
||||
},
|
||||
{
|
||||
id: 'signerName',
|
||||
title: 'Signer Name',
|
||||
type: 'short-input',
|
||||
placeholder: 'John Doe',
|
||||
condition: { field: 'operation', value: 'send_envelope' },
|
||||
required: { field: 'operation', value: 'send_envelope' },
|
||||
},
|
||||
{
|
||||
id: 'uploadDocument',
|
||||
title: 'Document',
|
||||
type: 'file-upload',
|
||||
canonicalParamId: 'documentFile',
|
||||
placeholder: 'Upload document for signature',
|
||||
mode: 'basic',
|
||||
multiple: false,
|
||||
condition: { field: 'operation', value: 'send_envelope' },
|
||||
},
|
||||
{
|
||||
id: 'documentRef',
|
||||
title: 'Document',
|
||||
type: 'short-input',
|
||||
canonicalParamId: 'documentFile',
|
||||
placeholder: 'Reference file from another block',
|
||||
mode: 'advanced',
|
||||
condition: { field: 'operation', value: 'send_envelope' },
|
||||
},
|
||||
{
|
||||
id: 'ccEmail',
|
||||
title: 'CC Email',
|
||||
type: 'short-input',
|
||||
placeholder: 'cc@example.com',
|
||||
condition: { field: 'operation', value: 'send_envelope' },
|
||||
mode: 'advanced',
|
||||
},
|
||||
{
|
||||
id: 'ccName',
|
||||
title: 'CC Name',
|
||||
type: 'short-input',
|
||||
placeholder: 'CC recipient name',
|
||||
condition: { field: 'operation', value: 'send_envelope' },
|
||||
mode: 'advanced',
|
||||
},
|
||||
{
|
||||
id: 'envelopeStatus',
|
||||
title: 'Status',
|
||||
type: 'dropdown',
|
||||
options: [
|
||||
{ label: 'Send Immediately', id: 'sent' },
|
||||
{ label: 'Save as Draft', id: 'created' },
|
||||
],
|
||||
value: () => 'sent',
|
||||
condition: { field: 'operation', value: ['send_envelope', 'create_from_template'] },
|
||||
mode: 'advanced',
|
||||
},
|
||||
|
||||
// Send from Template fields
|
||||
{
|
||||
id: 'templateId',
|
||||
title: 'Template ID',
|
||||
type: 'short-input',
|
||||
placeholder: 'DocuSign template ID',
|
||||
condition: { field: 'operation', value: 'create_from_template' },
|
||||
required: { field: 'operation', value: 'create_from_template' },
|
||||
},
|
||||
{
|
||||
id: 'templateRoles',
|
||||
title: 'Template Roles',
|
||||
type: 'long-input',
|
||||
placeholder: '[{"roleName":"Signer","name":"John Doe","email":"john@example.com"}]',
|
||||
condition: { field: 'operation', value: 'create_from_template' },
|
||||
required: { field: 'operation', value: 'create_from_template' },
|
||||
wandConfig: {
|
||||
enabled: true,
|
||||
prompt:
|
||||
'Generate a JSON array of DocuSign template role objects. Each role needs: roleName (must match the template role name), name (full name), email (email address). Return ONLY the JSON array.',
|
||||
generationType: 'json-object',
|
||||
},
|
||||
},
|
||||
|
||||
// Envelope ID field (shared across multiple operations)
|
||||
{
|
||||
id: 'envelopeId',
|
||||
title: 'Envelope ID',
|
||||
type: 'short-input',
|
||||
placeholder: 'DocuSign envelope ID',
|
||||
condition: {
|
||||
field: 'operation',
|
||||
value: ['get_envelope', 'void_envelope', 'download_document', 'list_recipients'],
|
||||
},
|
||||
required: {
|
||||
field: 'operation',
|
||||
value: ['get_envelope', 'void_envelope', 'download_document', 'list_recipients'],
|
||||
},
|
||||
},
|
||||
|
||||
// Void Envelope fields
|
||||
{
|
||||
id: 'voidedReason',
|
||||
title: 'Void Reason',
|
||||
type: 'short-input',
|
||||
placeholder: 'Reason for voiding this envelope',
|
||||
condition: { field: 'operation', value: 'void_envelope' },
|
||||
required: { field: 'operation', value: 'void_envelope' },
|
||||
},
|
||||
|
||||
// Download Document fields
|
||||
{
|
||||
id: 'documentId',
|
||||
title: 'Document ID',
|
||||
type: 'short-input',
|
||||
placeholder: '"combined" for all docs, or specific document ID',
|
||||
condition: { field: 'operation', value: 'download_document' },
|
||||
mode: 'advanced',
|
||||
},
|
||||
|
||||
// List Envelopes filters
|
||||
{
|
||||
id: 'fromDate',
|
||||
title: 'From Date',
|
||||
type: 'short-input',
|
||||
placeholder: 'ISO 8601 date (defaults to 30 days ago)',
|
||||
condition: { field: 'operation', value: 'list_envelopes' },
|
||||
mode: 'advanced',
|
||||
wandConfig: {
|
||||
enabled: true,
|
||||
prompt: 'Generate an ISO 8601 timestamp. Return ONLY the timestamp string.',
|
||||
generationType: 'timestamp',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'toDate',
|
||||
title: 'To Date',
|
||||
type: 'short-input',
|
||||
placeholder: 'ISO 8601 date',
|
||||
condition: { field: 'operation', value: 'list_envelopes' },
|
||||
mode: 'advanced',
|
||||
wandConfig: {
|
||||
enabled: true,
|
||||
prompt: 'Generate an ISO 8601 timestamp. Return ONLY the timestamp string.',
|
||||
generationType: 'timestamp',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'listEnvelopeStatus',
|
||||
title: 'Status Filter',
|
||||
type: 'dropdown',
|
||||
options: [
|
||||
{ label: 'All', id: '' },
|
||||
{ label: 'Created', id: 'created' },
|
||||
{ label: 'Sent', id: 'sent' },
|
||||
{ label: 'Delivered', id: 'delivered' },
|
||||
{ label: 'Completed', id: 'completed' },
|
||||
{ label: 'Declined', id: 'declined' },
|
||||
{ label: 'Voided', id: 'voided' },
|
||||
],
|
||||
value: () => '',
|
||||
condition: { field: 'operation', value: 'list_envelopes' },
|
||||
mode: 'advanced',
|
||||
},
|
||||
{
|
||||
id: 'searchText',
|
||||
title: 'Search',
|
||||
type: 'short-input',
|
||||
placeholder: 'Search envelopes or templates',
|
||||
condition: { field: 'operation', value: ['list_envelopes', 'list_templates'] },
|
||||
mode: 'advanced',
|
||||
},
|
||||
{
|
||||
id: 'count',
|
||||
title: 'Max Results',
|
||||
type: 'short-input',
|
||||
placeholder: '25',
|
||||
condition: { field: 'operation', value: ['list_envelopes', 'list_templates'] },
|
||||
mode: 'advanced',
|
||||
},
|
||||
],
|
||||
|
||||
tools: {
|
||||
access: [
|
||||
'docusign_send_envelope',
|
||||
'docusign_create_from_template',
|
||||
'docusign_get_envelope',
|
||||
'docusign_list_envelopes',
|
||||
'docusign_void_envelope',
|
||||
'docusign_download_document',
|
||||
'docusign_list_templates',
|
||||
'docusign_list_recipients',
|
||||
],
|
||||
config: {
|
||||
tool: (params) => `docusign_${params.operation}`,
|
||||
params: (params) => {
|
||||
const { oauthCredential, operation, documentFile, listEnvelopeStatus, ...rest } = params
|
||||
|
||||
const cleanParams: Record<string, unknown> = {
|
||||
oauthCredential,
|
||||
}
|
||||
|
||||
const file = normalizeFileInput(documentFile, { single: true })
|
||||
if (file) {
|
||||
cleanParams.file = file
|
||||
}
|
||||
|
||||
if (listEnvelopeStatus && operation === 'list_envelopes') {
|
||||
cleanParams.envelopeStatus = listEnvelopeStatus
|
||||
}
|
||||
|
||||
if (operation === 'create_from_template') {
|
||||
cleanParams.status = rest.envelopeStatus
|
||||
} else if (operation === 'send_envelope') {
|
||||
cleanParams.status = rest.envelopeStatus
|
||||
}
|
||||
|
||||
const excludeKeys = ['envelopeStatus']
|
||||
for (const [key, value] of Object.entries(rest)) {
|
||||
if (value !== undefined && value !== null && value !== '' && !excludeKeys.includes(key)) {
|
||||
cleanParams[key] = value
|
||||
}
|
||||
}
|
||||
|
||||
return cleanParams
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
inputs: {
|
||||
operation: { type: 'string', description: 'Operation to perform' },
|
||||
oauthCredential: { type: 'string', description: 'DocuSign access token' },
|
||||
emailSubject: { type: 'string', description: 'Email subject for the envelope' },
|
||||
emailBody: { type: 'string', description: 'Email body message' },
|
||||
signerEmail: { type: 'string', description: 'Signer email address' },
|
||||
signerName: { type: 'string', description: 'Signer full name' },
|
||||
documentFile: { type: 'string', description: 'Document file for signature' },
|
||||
ccEmail: { type: 'string', description: 'CC recipient email' },
|
||||
ccName: { type: 'string', description: 'CC recipient name' },
|
||||
templateId: { type: 'string', description: 'DocuSign template ID' },
|
||||
templateRoles: { type: 'string', description: 'JSON array of template roles' },
|
||||
envelopeId: { type: 'string', description: 'Envelope ID' },
|
||||
voidedReason: { type: 'string', description: 'Reason for voiding' },
|
||||
documentId: { type: 'string', description: 'Document ID to download' },
|
||||
fromDate: { type: 'string', description: 'Start date filter' },
|
||||
toDate: { type: 'string', description: 'End date filter' },
|
||||
searchText: { type: 'string', description: 'Search text filter' },
|
||||
count: { type: 'string', description: 'Max results to return' },
|
||||
},
|
||||
outputs: {
|
||||
envelopeId: { type: 'string', description: 'Envelope ID' },
|
||||
status: {
|
||||
type: 'string',
|
||||
description: 'Envelope status (created, sent, delivered, completed, declined, voided)',
|
||||
},
|
||||
statusDateTime: { type: 'string', description: 'ISO 8601 datetime of status change' },
|
||||
uri: { type: 'string', description: 'Envelope URI path' },
|
||||
emailSubject: { type: 'string', description: 'Envelope email subject' },
|
||||
sentDateTime: { type: 'string', description: 'ISO 8601 datetime when envelope was sent' },
|
||||
completedDateTime: { type: 'string', description: 'ISO 8601 datetime when signing completed' },
|
||||
createdDateTime: { type: 'string', description: 'ISO 8601 datetime when envelope was created' },
|
||||
statusChangedDateTime: {
|
||||
type: 'string',
|
||||
description: 'ISO 8601 datetime of last status change',
|
||||
},
|
||||
voidedReason: { type: 'string', description: 'Reason the envelope was voided' },
|
||||
signerCount: { type: 'number', description: 'Number of signers on the envelope' },
|
||||
documentCount: { type: 'number', description: 'Number of documents in the envelope' },
|
||||
envelopes: {
|
||||
type: 'json',
|
||||
description:
|
||||
'Array of envelopes (envelopeId, status, emailSubject, sentDateTime, completedDateTime, createdDateTime, statusChangedDateTime)',
|
||||
},
|
||||
templates: {
|
||||
type: 'json',
|
||||
description:
|
||||
'Array of templates (templateId, name, description, shared, created, lastModified)',
|
||||
},
|
||||
signers: {
|
||||
type: 'json',
|
||||
description:
|
||||
'Array of signer recipients (recipientId, name, email, status, signedDateTime, deliveredDateTime)',
|
||||
},
|
||||
carbonCopies: {
|
||||
type: 'json',
|
||||
description: 'Array of CC recipients (recipientId, name, email, status)',
|
||||
},
|
||||
base64Content: { type: 'string', description: 'Base64-encoded document content' },
|
||||
mimeType: { type: 'string', description: 'Document MIME type' },
|
||||
fileName: { type: 'string', description: 'Document file name' },
|
||||
totalSetSize: { type: 'number', description: 'Total matching results' },
|
||||
resultSetSize: { type: 'number', description: 'Results returned in this response' },
|
||||
},
|
||||
}
|
||||
440
apps/sim/blocks/blocks/workday.ts
Normal file
440
apps/sim/blocks/blocks/workday.ts
Normal file
@@ -0,0 +1,440 @@
|
||||
import { WorkdayIcon } from '@/components/icons'
|
||||
import type { BlockConfig } from '@/blocks/types'
|
||||
|
||||
export const WorkdayBlock: BlockConfig = {
|
||||
type: 'workday',
|
||||
name: 'Workday',
|
||||
description: 'Manage workers, hiring, onboarding, and HR operations in Workday',
|
||||
longDescription:
|
||||
'Integrate Workday HRIS into your workflow. Create pre-hires, hire employees, manage worker profiles, assign onboarding plans, handle job changes, retrieve compensation data, and process terminations.',
|
||||
docsLink: 'https://docs.sim.ai/tools/workday',
|
||||
category: 'tools',
|
||||
bgColor: '#F5F0EB',
|
||||
icon: WorkdayIcon,
|
||||
subBlocks: [
|
||||
{
|
||||
id: 'operation',
|
||||
title: 'Operation',
|
||||
type: 'dropdown',
|
||||
options: [
|
||||
{ label: 'Get Worker', id: 'get_worker' },
|
||||
{ label: 'List Workers', id: 'list_workers' },
|
||||
{ label: 'Create Pre-Hire', id: 'create_prehire' },
|
||||
{ label: 'Hire Employee', id: 'hire_employee' },
|
||||
{ label: 'Update Worker', id: 'update_worker' },
|
||||
{ label: 'Assign Onboarding Plan', id: 'assign_onboarding' },
|
||||
{ label: 'Get Organizations', id: 'get_organizations' },
|
||||
{ label: 'Change Job', id: 'change_job' },
|
||||
{ label: 'Get Compensation', id: 'get_compensation' },
|
||||
{ label: 'Terminate Worker', id: 'terminate_worker' },
|
||||
],
|
||||
value: () => 'get_worker',
|
||||
},
|
||||
{
|
||||
id: 'tenantUrl',
|
||||
title: 'Tenant URL',
|
||||
type: 'short-input',
|
||||
placeholder: 'https://wd2-impl-services1.workday.com',
|
||||
required: true,
|
||||
description: 'Your Workday instance URL (e.g., https://wd2-impl-services1.workday.com)',
|
||||
},
|
||||
{
|
||||
id: 'tenant',
|
||||
title: 'Tenant Name',
|
||||
type: 'short-input',
|
||||
placeholder: 'mycompany',
|
||||
required: true,
|
||||
description: 'Workday tenant identifier',
|
||||
},
|
||||
{
|
||||
id: 'username',
|
||||
title: 'Username',
|
||||
type: 'short-input',
|
||||
placeholder: 'ISU username',
|
||||
required: true,
|
||||
description: 'Integration System User username',
|
||||
},
|
||||
{
|
||||
id: 'password',
|
||||
title: 'Password',
|
||||
type: 'short-input',
|
||||
placeholder: 'ISU password',
|
||||
password: true,
|
||||
required: true,
|
||||
description: 'Integration System User password',
|
||||
},
|
||||
|
||||
// Get Worker
|
||||
{
|
||||
id: 'workerId',
|
||||
title: 'Worker ID',
|
||||
type: 'short-input',
|
||||
placeholder: 'e.g., 3aa5550b7fe348b98d7b5741afc65534',
|
||||
condition: {
|
||||
field: 'operation',
|
||||
value: [
|
||||
'get_worker',
|
||||
'update_worker',
|
||||
'assign_onboarding',
|
||||
'change_job',
|
||||
'get_compensation',
|
||||
'terminate_worker',
|
||||
],
|
||||
},
|
||||
required: {
|
||||
field: 'operation',
|
||||
value: [
|
||||
'get_worker',
|
||||
'update_worker',
|
||||
'assign_onboarding',
|
||||
'change_job',
|
||||
'get_compensation',
|
||||
'terminate_worker',
|
||||
],
|
||||
},
|
||||
},
|
||||
|
||||
// List Workers
|
||||
{
|
||||
id: 'limit',
|
||||
title: 'Limit',
|
||||
type: 'short-input',
|
||||
placeholder: '20',
|
||||
condition: { field: 'operation', value: ['list_workers', 'get_organizations'] },
|
||||
mode: 'advanced',
|
||||
},
|
||||
{
|
||||
id: 'offset',
|
||||
title: 'Offset',
|
||||
type: 'short-input',
|
||||
placeholder: '0',
|
||||
condition: { field: 'operation', value: ['list_workers', 'get_organizations'] },
|
||||
mode: 'advanced',
|
||||
},
|
||||
|
||||
// Create Pre-Hire
|
||||
{
|
||||
id: 'legalName',
|
||||
title: 'Legal Name',
|
||||
type: 'short-input',
|
||||
placeholder: 'e.g., Jane Doe',
|
||||
condition: { field: 'operation', value: 'create_prehire' },
|
||||
required: { field: 'operation', value: 'create_prehire' },
|
||||
},
|
||||
{
|
||||
id: 'email',
|
||||
title: 'Email',
|
||||
type: 'short-input',
|
||||
placeholder: 'jane.doe@company.com',
|
||||
condition: { field: 'operation', value: 'create_prehire' },
|
||||
},
|
||||
{
|
||||
id: 'phoneNumber',
|
||||
title: 'Phone Number',
|
||||
type: 'short-input',
|
||||
placeholder: '+1-555-0100',
|
||||
condition: { field: 'operation', value: 'create_prehire' },
|
||||
mode: 'advanced',
|
||||
},
|
||||
{
|
||||
id: 'address',
|
||||
title: 'Address',
|
||||
type: 'short-input',
|
||||
placeholder: '123 Main St, City, State',
|
||||
condition: { field: 'operation', value: 'create_prehire' },
|
||||
mode: 'advanced',
|
||||
},
|
||||
{
|
||||
id: 'countryCode',
|
||||
title: 'Country Code',
|
||||
type: 'short-input',
|
||||
placeholder: 'US',
|
||||
condition: { field: 'operation', value: 'create_prehire' },
|
||||
mode: 'advanced',
|
||||
description: 'ISO 3166-1 Alpha-2 country code (defaults to US)',
|
||||
},
|
||||
|
||||
// Hire Employee
|
||||
{
|
||||
id: 'preHireId',
|
||||
title: 'Pre-Hire ID',
|
||||
type: 'short-input',
|
||||
placeholder: 'Pre-hire record ID',
|
||||
condition: { field: 'operation', value: 'hire_employee' },
|
||||
required: { field: 'operation', value: 'hire_employee' },
|
||||
},
|
||||
{
|
||||
id: 'positionId',
|
||||
title: 'Position ID',
|
||||
type: 'short-input',
|
||||
placeholder: 'Position to assign',
|
||||
condition: { field: 'operation', value: ['hire_employee', 'change_job'] },
|
||||
required: { field: 'operation', value: ['hire_employee'] },
|
||||
},
|
||||
{
|
||||
id: 'hireDate',
|
||||
title: 'Hire Date',
|
||||
type: 'short-input',
|
||||
placeholder: 'YYYY-MM-DD',
|
||||
condition: { field: 'operation', value: 'hire_employee' },
|
||||
required: { field: 'operation', value: 'hire_employee' },
|
||||
wandConfig: {
|
||||
enabled: true,
|
||||
prompt: 'Generate an ISO 8601 date (YYYY-MM-DD). Return ONLY the date string.',
|
||||
generationType: 'timestamp',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'jobProfileId',
|
||||
title: 'Job Profile ID',
|
||||
type: 'short-input',
|
||||
placeholder: 'Job profile ID',
|
||||
condition: { field: 'operation', value: 'change_job' },
|
||||
mode: 'advanced',
|
||||
},
|
||||
{
|
||||
id: 'locationId',
|
||||
title: 'Location ID',
|
||||
type: 'short-input',
|
||||
placeholder: 'Work location ID',
|
||||
condition: { field: 'operation', value: 'change_job' },
|
||||
mode: 'advanced',
|
||||
},
|
||||
{
|
||||
id: 'supervisoryOrgId',
|
||||
title: 'Supervisory Organization ID',
|
||||
type: 'short-input',
|
||||
placeholder: 'Target supervisory organization ID',
|
||||
condition: { field: 'operation', value: 'change_job' },
|
||||
mode: 'advanced',
|
||||
},
|
||||
{
|
||||
id: 'employeeType',
|
||||
title: 'Employee Type',
|
||||
type: 'dropdown',
|
||||
options: [
|
||||
{ label: 'Regular', id: 'Regular' },
|
||||
{ label: 'Temporary', id: 'Temporary' },
|
||||
{ label: 'Contractor', id: 'Contractor' },
|
||||
],
|
||||
value: () => 'Regular',
|
||||
condition: { field: 'operation', value: 'hire_employee' },
|
||||
mode: 'advanced',
|
||||
},
|
||||
|
||||
// Update Worker
|
||||
{
|
||||
id: 'fields',
|
||||
title: 'Fields (JSON)',
|
||||
type: 'code',
|
||||
language: 'json',
|
||||
placeholder:
|
||||
'{\n "businessTitle": "Senior Engineer",\n "primaryWorkEmail": "new@company.com"\n}',
|
||||
condition: { field: 'operation', value: 'update_worker' },
|
||||
required: { field: 'operation', value: 'update_worker' },
|
||||
wandConfig: {
|
||||
enabled: true,
|
||||
maintainHistory: true,
|
||||
prompt: `Generate a Workday worker update payload as JSON.
|
||||
|
||||
### COMMON FIELDS
|
||||
- businessTitle: Job title string
|
||||
- primaryWorkEmail: Work email address
|
||||
- primaryWorkPhone: Work phone number
|
||||
- managerReference: Manager worker ID
|
||||
|
||||
### RULES
|
||||
- Output ONLY valid JSON starting with { and ending with }
|
||||
- Include only fields that need updating
|
||||
|
||||
### EXAMPLE
|
||||
User: "Update title to Senior Engineer"
|
||||
Output: {"businessTitle": "Senior Engineer"}`,
|
||||
generationType: 'json-object',
|
||||
},
|
||||
},
|
||||
|
||||
// Assign Onboarding
|
||||
{
|
||||
id: 'onboardingPlanId',
|
||||
title: 'Onboarding Plan ID',
|
||||
type: 'short-input',
|
||||
placeholder: 'Plan ID to assign',
|
||||
condition: { field: 'operation', value: 'assign_onboarding' },
|
||||
required: { field: 'operation', value: 'assign_onboarding' },
|
||||
},
|
||||
{
|
||||
id: 'actionEventId',
|
||||
title: 'Action Event ID',
|
||||
type: 'short-input',
|
||||
placeholder: 'Hiring event ID that enables onboarding',
|
||||
condition: { field: 'operation', value: 'assign_onboarding' },
|
||||
required: { field: 'operation', value: 'assign_onboarding' },
|
||||
},
|
||||
|
||||
// Get Organizations
|
||||
{
|
||||
id: 'orgType',
|
||||
title: 'Organization Type',
|
||||
type: 'dropdown',
|
||||
options: [
|
||||
{ label: 'All Types', id: '' },
|
||||
{ label: 'Supervisory', id: 'Supervisory' },
|
||||
{ label: 'Cost Center', id: 'Cost_Center' },
|
||||
{ label: 'Company', id: 'Company' },
|
||||
{ label: 'Region', id: 'Region' },
|
||||
],
|
||||
value: () => '',
|
||||
condition: { field: 'operation', value: 'get_organizations' },
|
||||
},
|
||||
|
||||
// Change Job
|
||||
{
|
||||
id: 'effectiveDate',
|
||||
title: 'Effective Date',
|
||||
type: 'short-input',
|
||||
placeholder: 'YYYY-MM-DD',
|
||||
condition: { field: 'operation', value: 'change_job' },
|
||||
required: { field: 'operation', value: 'change_job' },
|
||||
wandConfig: {
|
||||
enabled: true,
|
||||
prompt: 'Generate an ISO 8601 date (YYYY-MM-DD). Return ONLY the date string.',
|
||||
generationType: 'timestamp',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'reason',
|
||||
title: 'Reason',
|
||||
type: 'short-input',
|
||||
placeholder: 'e.g., Promotion, Transfer',
|
||||
condition: { field: 'operation', value: ['change_job', 'terminate_worker'] },
|
||||
required: { field: 'operation', value: ['change_job', 'terminate_worker'] },
|
||||
},
|
||||
|
||||
// Terminate Worker
|
||||
{
|
||||
id: 'terminationDate',
|
||||
title: 'Termination Date',
|
||||
type: 'short-input',
|
||||
placeholder: 'YYYY-MM-DD',
|
||||
condition: { field: 'operation', value: 'terminate_worker' },
|
||||
required: { field: 'operation', value: 'terminate_worker' },
|
||||
wandConfig: {
|
||||
enabled: true,
|
||||
prompt: 'Generate an ISO 8601 date (YYYY-MM-DD). Return ONLY the date string.',
|
||||
generationType: 'timestamp',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'notificationDate',
|
||||
title: 'Notification Date',
|
||||
type: 'short-input',
|
||||
placeholder: 'YYYY-MM-DD',
|
||||
condition: { field: 'operation', value: 'terminate_worker' },
|
||||
mode: 'advanced',
|
||||
},
|
||||
{
|
||||
id: 'lastDayOfWork',
|
||||
title: 'Last Day of Work',
|
||||
type: 'short-input',
|
||||
placeholder: 'YYYY-MM-DD (defaults to termination date)',
|
||||
condition: { field: 'operation', value: 'terminate_worker' },
|
||||
mode: 'advanced',
|
||||
},
|
||||
],
|
||||
tools: {
|
||||
access: [
|
||||
'workday_get_worker',
|
||||
'workday_list_workers',
|
||||
'workday_create_prehire',
|
||||
'workday_hire_employee',
|
||||
'workday_update_worker',
|
||||
'workday_assign_onboarding',
|
||||
'workday_get_organizations',
|
||||
'workday_change_job',
|
||||
'workday_get_compensation',
|
||||
'workday_terminate_worker',
|
||||
],
|
||||
config: {
|
||||
tool: (params) => `workday_${params.operation}`,
|
||||
params: (params) => {
|
||||
const { operation, orgType, fields, jobProfileId, locationId, supervisoryOrgId, ...rest } =
|
||||
params
|
||||
|
||||
if (rest.limit != null && rest.limit !== '') rest.limit = Number(rest.limit)
|
||||
if (rest.offset != null && rest.offset !== '') rest.offset = Number(rest.offset)
|
||||
|
||||
if (orgType) rest.type = orgType
|
||||
|
||||
if (operation === 'change_job') {
|
||||
if (rest.positionId) {
|
||||
rest.newPositionId = rest.positionId
|
||||
rest.positionId = undefined
|
||||
}
|
||||
if (jobProfileId) rest.newJobProfileId = jobProfileId
|
||||
if (locationId) rest.newLocationId = locationId
|
||||
if (supervisoryOrgId) rest.newSupervisoryOrgId = supervisoryOrgId
|
||||
}
|
||||
|
||||
if (fields && operation === 'update_worker') {
|
||||
try {
|
||||
const parsedFields = typeof fields === 'string' ? JSON.parse(fields) : fields
|
||||
return { ...rest, fields: parsedFields }
|
||||
} catch {
|
||||
throw new Error('Invalid JSON in Fields block')
|
||||
}
|
||||
}
|
||||
|
||||
return rest
|
||||
},
|
||||
},
|
||||
},
|
||||
inputs: {
|
||||
operation: { type: 'string', description: 'Workday operation to perform' },
|
||||
tenantUrl: { type: 'string', description: 'Workday instance URL' },
|
||||
tenant: { type: 'string', description: 'Workday tenant name' },
|
||||
username: { type: 'string', description: 'ISU username' },
|
||||
password: { type: 'string', description: 'ISU password' },
|
||||
workerId: { type: 'string', description: 'Worker ID' },
|
||||
limit: { type: 'number', description: 'Result limit' },
|
||||
offset: { type: 'number', description: 'Pagination offset' },
|
||||
legalName: { type: 'string', description: 'Legal name for pre-hire' },
|
||||
email: { type: 'string', description: 'Email address' },
|
||||
phoneNumber: { type: 'string', description: 'Phone number' },
|
||||
address: { type: 'string', description: 'Address' },
|
||||
countryCode: { type: 'string', description: 'ISO 3166-1 Alpha-2 country code' },
|
||||
preHireId: { type: 'string', description: 'Pre-hire record ID' },
|
||||
positionId: { type: 'string', description: 'Position ID' },
|
||||
hireDate: { type: 'string', description: 'Hire date (YYYY-MM-DD)' },
|
||||
jobProfileId: { type: 'string', description: 'Job profile ID' },
|
||||
locationId: { type: 'string', description: 'Location ID' },
|
||||
supervisoryOrgId: { type: 'string', description: 'Target supervisory organization ID' },
|
||||
employeeType: { type: 'string', description: 'Employee type' },
|
||||
fields: { type: 'json', description: 'Fields to update' },
|
||||
onboardingPlanId: { type: 'string', description: 'Onboarding plan ID' },
|
||||
actionEventId: { type: 'string', description: 'Action event ID for onboarding' },
|
||||
orgType: { type: 'string', description: 'Organization type filter' },
|
||||
effectiveDate: { type: 'string', description: 'Effective date (YYYY-MM-DD)' },
|
||||
reason: { type: 'string', description: 'Reason for change or termination' },
|
||||
terminationDate: { type: 'string', description: 'Termination date (YYYY-MM-DD)' },
|
||||
notificationDate: { type: 'string', description: 'Notification date' },
|
||||
lastDayOfWork: { type: 'string', description: 'Last day of work' },
|
||||
},
|
||||
outputs: {
|
||||
worker: { type: 'json', description: 'Worker profile data' },
|
||||
workers: { type: 'json', description: 'Array of worker profiles' },
|
||||
total: { type: 'number', description: 'Total count of results' },
|
||||
preHireId: { type: 'string', description: 'Created pre-hire ID' },
|
||||
descriptor: { type: 'string', description: 'Display name of pre-hire' },
|
||||
workerId: { type: 'string', description: 'Worker ID' },
|
||||
employeeId: { type: 'string', description: 'Employee ID' },
|
||||
hireDate: { type: 'string', description: 'Hire date' },
|
||||
assignmentId: { type: 'string', description: 'Onboarding assignment ID' },
|
||||
planId: { type: 'string', description: 'Onboarding plan ID' },
|
||||
organizations: { type: 'json', description: 'Array of organizations' },
|
||||
eventId: { type: 'string', description: 'Event ID for staffing changes' },
|
||||
effectiveDate: { type: 'string', description: 'Effective date of change' },
|
||||
compensationPlans: { type: 'json', description: 'Compensation plan details' },
|
||||
terminationDate: { type: 'string', description: 'Termination date' },
|
||||
},
|
||||
}
|
||||
@@ -13,6 +13,7 @@ import { ArxivBlock } from '@/blocks/blocks/arxiv'
|
||||
import { AsanaBlock } from '@/blocks/blocks/asana'
|
||||
import { AshbyBlock } from '@/blocks/blocks/ashby'
|
||||
import { AttioBlock } from '@/blocks/blocks/attio'
|
||||
import { BoxBlock } from '@/blocks/blocks/box'
|
||||
import { BrandfetchBlock } from '@/blocks/blocks/brandfetch'
|
||||
import { BrowserUseBlock } from '@/blocks/blocks/browser_use'
|
||||
import { CalComBlock } from '@/blocks/blocks/calcom'
|
||||
@@ -29,6 +30,7 @@ import { DatabricksBlock } from '@/blocks/blocks/databricks'
|
||||
import { DatadogBlock } from '@/blocks/blocks/datadog'
|
||||
import { DevinBlock } from '@/blocks/blocks/devin'
|
||||
import { DiscordBlock } from '@/blocks/blocks/discord'
|
||||
import { DocuSignBlock } from '@/blocks/blocks/docusign'
|
||||
import { DropboxBlock } from '@/blocks/blocks/dropbox'
|
||||
import { DSPyBlock } from '@/blocks/blocks/dspy'
|
||||
import { DubBlock } from '@/blocks/blocks/dub'
|
||||
@@ -188,6 +190,7 @@ import { WebhookRequestBlock } from '@/blocks/blocks/webhook_request'
|
||||
import { WhatsAppBlock } from '@/blocks/blocks/whatsapp'
|
||||
import { WikipediaBlock } from '@/blocks/blocks/wikipedia'
|
||||
import { WordPressBlock } from '@/blocks/blocks/wordpress'
|
||||
import { WorkdayBlock } from '@/blocks/blocks/workday'
|
||||
import { WorkflowBlock } from '@/blocks/blocks/workflow'
|
||||
import { WorkflowInputBlock } from '@/blocks/blocks/workflow_input'
|
||||
import { XBlock } from '@/blocks/blocks/x'
|
||||
@@ -215,6 +218,7 @@ export const registry: Record<string, BlockConfig> = {
|
||||
ashby: AshbyBlock,
|
||||
attio: AttioBlock,
|
||||
brandfetch: BrandfetchBlock,
|
||||
box: BoxBlock,
|
||||
browser_use: BrowserUseBlock,
|
||||
calcom: CalComBlock,
|
||||
calendly: CalendlyBlock,
|
||||
@@ -232,6 +236,7 @@ export const registry: Record<string, BlockConfig> = {
|
||||
datadog: DatadogBlock,
|
||||
devin: DevinBlock,
|
||||
discord: DiscordBlock,
|
||||
docusign: DocuSignBlock,
|
||||
dropbox: DropboxBlock,
|
||||
dspy: DSPyBlock,
|
||||
dub: DubBlock,
|
||||
@@ -408,6 +413,7 @@ export const registry: Record<string, BlockConfig> = {
|
||||
whatsapp: WhatsAppBlock,
|
||||
wikipedia: WikipediaBlock,
|
||||
wordpress: WordPressBlock,
|
||||
workday: WorkdayBlock,
|
||||
workflow: WorkflowBlock,
|
||||
workflow_input: WorkflowInputBlock,
|
||||
x: XBlock,
|
||||
|
||||
@@ -124,6 +124,34 @@ export function NoteIcon(props: SVGProps<SVGSVGElement>) {
|
||||
)
|
||||
}
|
||||
|
||||
export function WorkdayIcon(props: SVGProps<SVGSVGElement>) {
|
||||
const id = useId()
|
||||
const clipId = `workday_clip_${id}`
|
||||
return (
|
||||
<svg {...props} viewBox='0 0 64 64' fill='none' xmlns='http://www.w3.org/2000/svg'>
|
||||
<g clipPath={`url(#${clipId})`} transform='matrix(0.53333333,0,0,0.53333333,-124.63685,-16)'>
|
||||
<path
|
||||
fillRule='evenodd'
|
||||
clipRule='evenodd'
|
||||
d='m 251.21,88.7755 h 8.224 c 1.166,0 2.178,0.7836 2.444,1.8924 l 11.057,44.6751 c 0.152,0.002 12.182,-44.6393 12.182,-44.6393 0.306,-1.1361 1.36,-1.9282 2.566,-1.9282 h 12.74 c 1.144,0 2.144,0.7515 2.435,1.8296 l 12.118,44.9289 c 0.448,-0.282 11.147,-44.8661 11.147,-44.8661 0.267,-1.1088 1.279,-1.8924 2.444,-1.8924 h 8.219 c 1.649,0 2.854,1.5192 2.437,3.0742 l -15.08,56.3173 c -0.286,1.072 -1.272,1.823 -2.406,1.833 l -12.438,-0.019 c -1.142,-0.002 -2.137,-0.744 -2.429,-1.819 -2.126,-7.805 -12.605,-47.277 -12.605,-47.277 0,0 -11.008,39.471 -13.133,47.277 -0.293,1.075 -1.288,1.817 -2.429,1.819 L 266.264,150 c -1.133,-0.01 -2.119,-0.761 -2.406,-1.833 L 248.777,91.8438 c -0.416,-1.5524 0.786,-3.0683 2.433,-3.0683 z'
|
||||
fill='#005cb9'
|
||||
/>
|
||||
<path
|
||||
fillRule='evenodd'
|
||||
clipRule='evenodd'
|
||||
d='m 333.324,72.2449 c 0.531,0 1.071,-0.0723 1.608,-0.2234 3.18,-0.8968 5.039,-4.2303 4.153,-7.446 -0.129,-0.4673 -0.265,-0.9327 -0.408,-1.3936 C 332.529,43.3349 314.569,30 293.987,30 c -20.557,0 -38.51,13.3133 -44.673,33.1281 -0.136,0.4355 -0.267,0.8782 -0.391,1.3232 -0.902,3.2119 0.943,6.5541 4.12,7.4645 3.173,0.9112 6.48,-0.9547 7.381,-4.1666 0.094,-0.3322 0.19,-0.6616 0.292,-0.9892 4.591,-14.7582 17.961,-24.6707 33.271,-24.6707 15.329,0 28.704,9.9284 33.281,24.7063 0.105,0.3397 0.206,0.682 0.301,1.0263 0.737,2.6726 3.139,4.423 5.755,4.423 z'
|
||||
fill='#f38b00'
|
||||
/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id={clipId}>
|
||||
<path d='M 354,30 H 234 v 120 h 120 z' fill='#ffffff' />
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
export function WorkflowIcon(props: SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
<svg
|
||||
@@ -1146,6 +1174,25 @@ export function DevinIcon(props: SVGProps<SVGSVGElement>) {
|
||||
)
|
||||
}
|
||||
|
||||
export function DocuSignIcon(props: SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
<svg {...props} viewBox='0 0 1547 1549' xmlns='http://www.w3.org/2000/svg'>
|
||||
<path
|
||||
d='m1113.4 1114.9v395.6c0 20.8-16.7 37.6-37.5 37.6h-1038.4c-20.7 0-37.5-16.8-37.5-37.6v-1039c0-20.7 16.8-37.5 37.5-37.5h394.3v643.4c0 20.7 16.8 37.5 37.5 37.5z'
|
||||
fill='#4c00ff'
|
||||
/>
|
||||
<path
|
||||
d='m1546 557.1c0 332.4-193.9 557-432.6 557.8v-418.8c0-12-4.8-24-13.5-31.9l-217.1-217.4c-8.8-8.8-20-13.6-32-13.6h-418.2v-394.8c0-20.8 16.8-37.6 37.5-37.6h585.1c277.7-0.8 490.8 223 490.8 556.3z'
|
||||
fill='#ff5252'
|
||||
/>
|
||||
<path
|
||||
d='m1099.9 663.4c8.7 8.7 13.5 19.9 13.5 31.9v418.8h-643.3c-20.7 0-37.5-16.8-37.5-37.5v-643.4h418.2c12 0 24 4.8 32 13.6z'
|
||||
fill='#000000'
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
export function DiscordIcon(props: SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
<svg
|
||||
@@ -4569,11 +4616,17 @@ export function ShopifyIcon(props: SVGProps<SVGSVGElement>) {
|
||||
|
||||
export function BoxCompanyIcon(props: SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
<svg {...props} xmlns='http://www.w3.org/2000/svg' viewBox='0 0 41 22'>
|
||||
<path
|
||||
d='M39.7 19.2c.5.7.4 1.6-.2 2.1-.7.5-1.7.4-2.2-.2l-3.5-4.5-3.4 4.4c-.5.7-1.5.7-2.2.2-.7-.5-.8-1.4-.3-2.1l4-5.2-4-5.2c-.5-.7-.3-1.7.3-2.2.7-.5 1.7-.3 2.2.3l3.4 4.5L37.3 7c.5-.7 1.4-.8 2.2-.3.7.5.7 1.5.2 2.2L35.8 14l3.9 5.2zm-18.2-.6c-2.6 0-4.7-2-4.7-4.6 0-2.5 2.1-4.6 4.7-4.6s4.7 2.1 4.7 4.6c-.1 2.6-2.2 4.6-4.7 4.6zm-13.8 0c-2.6 0-4.7-2-4.7-4.6 0-2.5 2.1-4.6 4.7-4.6s4.7 2.1 4.7 4.6c0 2.6-2.1 4.6-4.7 4.6zM21.5 6.4c-2.9 0-5.5 1.6-6.8 4-1.3-2.4-3.9-4-6.9-4-1.8 0-3.4.6-4.7 1.5V1.5C3.1.7 2.4 0 1.6 0 .7 0 0 .7 0 1.5v12.6c.1 4.2 3.5 7.5 7.7 7.5 3 0 5.6-1.7 6.9-4.1 1.3 2.4 3.9 4.1 6.8 4.1 4.3 0 7.8-3.4 7.8-7.7.1-4.1-3.4-7.5-7.7-7.5z'
|
||||
fill='currentColor'
|
||||
/>
|
||||
<svg
|
||||
{...props}
|
||||
xmlns='http://www.w3.org/2000/svg'
|
||||
width='2500'
|
||||
height='1379'
|
||||
viewBox='0 0 444.893 245.414'
|
||||
>
|
||||
<g fill='#0075C9'>
|
||||
<path d='M239.038 72.43c-33.081 0-61.806 18.6-76.322 45.904-14.516-27.305-43.24-45.902-76.32-45.902-19.443 0-37.385 6.424-51.821 17.266V16.925h-.008C34.365 7.547 26.713 0 17.286 0 7.858 0 .208 7.547.008 16.925H0v143.333h.036c.768 47.051 39.125 84.967 86.359 84.967 33.08 0 61.805-18.603 76.32-45.908 14.517 27.307 43.241 45.906 76.321 45.906 47.715 0 86.396-38.684 86.396-86.396.001-47.718-38.682-86.397-86.394-86.397zM86.395 210.648c-28.621 0-51.821-23.201-51.821-51.82 0-28.623 23.201-51.823 51.821-51.823 28.621 0 51.822 23.2 51.822 51.823 0 28.619-23.201 51.82-51.822 51.82zm152.643 0c-28.622 0-51.821-23.201-51.821-51.822 0-28.623 23.2-51.821 51.821-51.821 28.619 0 51.822 23.198 51.822 51.821-.001 28.621-23.203 51.822-51.822 51.822z' />
|
||||
<path d='M441.651 218.033l-44.246-59.143 44.246-59.144-.008-.007c5.473-7.62 3.887-18.249-3.652-23.913-7.537-5.658-18.187-4.221-23.98 3.157l-.004-.002-38.188 51.047-38.188-51.047-.006.009c-5.793-7.385-16.441-8.822-23.981-3.16-7.539 5.664-9.125 16.293-3.649 23.911l-.008.005 44.245 59.144-44.245 59.143.008.005c-5.477 7.62-3.89 18.247 3.649 23.909 7.54 5.664 18.188 4.225 23.981-3.155l.006.007 38.188-51.049 38.188 51.049.004-.002c5.794 7.377 16.443 8.814 23.98 3.154 7.539-5.662 9.125-16.291 3.652-23.91l.008-.008z' />
|
||||
</g>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -92,6 +92,57 @@ describe('DAGBuilder disabled subflow validation', () => {
|
||||
// Should not throw - loop is effectively disabled since all inner blocks are disabled
|
||||
expect(() => builder.build(workflow)).not.toThrow()
|
||||
})
|
||||
|
||||
it('does not mutate serialized loop config nodes during DAG build', () => {
|
||||
const workflow: SerializedWorkflow = {
|
||||
version: '1',
|
||||
blocks: [
|
||||
createBlock('start', BlockType.STARTER),
|
||||
createBlock('loop-1', BlockType.LOOP),
|
||||
{ ...createBlock('inner-block', BlockType.FUNCTION), enabled: false },
|
||||
],
|
||||
connections: [{ source: 'start', target: 'loop-1' }],
|
||||
loops: {
|
||||
'loop-1': {
|
||||
id: 'loop-1',
|
||||
nodes: ['inner-block'],
|
||||
iterations: 3,
|
||||
},
|
||||
},
|
||||
parallels: {},
|
||||
}
|
||||
|
||||
const builder = new DAGBuilder()
|
||||
builder.build(workflow)
|
||||
|
||||
expect(workflow.loops?.['loop-1']?.nodes).toEqual(['inner-block'])
|
||||
})
|
||||
|
||||
it('does not mutate serialized parallel config nodes during DAG build', () => {
|
||||
const workflow: SerializedWorkflow = {
|
||||
version: '1',
|
||||
blocks: [
|
||||
createBlock('start', BlockType.STARTER),
|
||||
createBlock('parallel-1', BlockType.PARALLEL),
|
||||
{ ...createBlock('inner-block', BlockType.FUNCTION), enabled: false },
|
||||
],
|
||||
connections: [{ source: 'start', target: 'parallel-1' }],
|
||||
loops: {},
|
||||
parallels: {
|
||||
'parallel-1': {
|
||||
id: 'parallel-1',
|
||||
nodes: ['inner-block'],
|
||||
count: 2,
|
||||
parallelType: 'count',
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
const builder = new DAGBuilder()
|
||||
builder.build(workflow)
|
||||
|
||||
expect(workflow.parallels?.['parallel-1']?.nodes).toEqual(['inner-block'])
|
||||
})
|
||||
})
|
||||
|
||||
describe('DAGBuilder nested parallel support', () => {
|
||||
|
||||
@@ -113,13 +113,19 @@ export class DAGBuilder {
|
||||
private initializeConfigs(workflow: SerializedWorkflow, dag: DAG): void {
|
||||
if (workflow.loops) {
|
||||
for (const [loopId, loopConfig] of Object.entries(workflow.loops)) {
|
||||
dag.loopConfigs.set(loopId, loopConfig)
|
||||
dag.loopConfigs.set(loopId, {
|
||||
...loopConfig,
|
||||
nodes: [...(loopConfig.nodes ?? [])],
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if (workflow.parallels) {
|
||||
for (const [parallelId, parallelConfig] of Object.entries(workflow.parallels)) {
|
||||
dag.parallelConfigs.set(parallelId, parallelConfig)
|
||||
dag.parallelConfigs.set(parallelId, {
|
||||
...parallelConfig,
|
||||
nodes: [...(parallelConfig.nodes ?? [])],
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -150,9 +156,7 @@ export class DAGBuilder {
|
||||
if (!sentinelStartNode) return
|
||||
|
||||
if (!nodes || nodes.length === 0) {
|
||||
throw new Error(
|
||||
`${type} has no blocks inside. Add at least one block to the ${type.toLowerCase()}.`
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
const hasConnections = Array.from(sentinelStartNode.outgoingEdges.values()).some((edge) =>
|
||||
|
||||
@@ -8,24 +8,21 @@ const logger = createLogger('LoopConstructor')
|
||||
export class LoopConstructor {
|
||||
execute(dag: DAG, reachableBlocks: Set<string>): void {
|
||||
for (const [loopId, loopConfig] of dag.loopConfigs) {
|
||||
const loopNodes = loopConfig.nodes
|
||||
|
||||
if (loopNodes.length === 0) {
|
||||
if (!reachableBlocks.has(loopId)) {
|
||||
continue
|
||||
}
|
||||
|
||||
if (!this.hasReachableNodes(loopNodes, reachableBlocks)) {
|
||||
continue
|
||||
const loopNodes = loopConfig.nodes
|
||||
const hasReachableChildren = loopNodes.some((nodeId) => reachableBlocks.has(nodeId))
|
||||
|
||||
if (!hasReachableChildren) {
|
||||
loopConfig.nodes = []
|
||||
}
|
||||
|
||||
this.createSentinelPair(dag, loopId)
|
||||
}
|
||||
}
|
||||
|
||||
private hasReachableNodes(loopNodes: string[], reachableBlocks: Set<string>): boolean {
|
||||
return loopNodes.some((nodeId) => reachableBlocks.has(nodeId))
|
||||
}
|
||||
|
||||
private createSentinelPair(dag: DAG, loopId: string): void {
|
||||
const startId = buildSentinelStartId(loopId)
|
||||
const endId = buildSentinelEndId(loopId)
|
||||
|
||||
@@ -11,24 +11,21 @@ const logger = createLogger('ParallelConstructor')
|
||||
export class ParallelConstructor {
|
||||
execute(dag: DAG, reachableBlocks: Set<string>): void {
|
||||
for (const [parallelId, parallelConfig] of dag.parallelConfigs) {
|
||||
const parallelNodes = parallelConfig.nodes
|
||||
|
||||
if (parallelNodes.length === 0) {
|
||||
if (!reachableBlocks.has(parallelId)) {
|
||||
continue
|
||||
}
|
||||
|
||||
if (!this.hasReachableNodes(parallelNodes, reachableBlocks)) {
|
||||
continue
|
||||
const parallelNodes = parallelConfig.nodes
|
||||
const hasReachableChildren = parallelNodes.some((nodeId) => reachableBlocks.has(nodeId))
|
||||
|
||||
if (!hasReachableChildren) {
|
||||
parallelConfig.nodes = []
|
||||
}
|
||||
|
||||
this.createSentinelPair(dag, parallelId)
|
||||
}
|
||||
}
|
||||
|
||||
private hasReachableNodes(parallelNodes: string[], reachableBlocks: Set<string>): boolean {
|
||||
return parallelNodes.some((nodeId) => reachableBlocks.has(nodeId))
|
||||
}
|
||||
|
||||
private createSentinelPair(dag: DAG, parallelId: string): void {
|
||||
const startId = buildParallelSentinelStartId(parallelId)
|
||||
const endId = buildParallelSentinelEndId(parallelId)
|
||||
|
||||
@@ -56,6 +56,29 @@ export class LoopOrchestrator {
|
||||
if (!loopConfig) {
|
||||
throw new Error(`Loop config not found: ${loopId}`)
|
||||
}
|
||||
|
||||
if (loopConfig.nodes.length === 0) {
|
||||
const errorMessage =
|
||||
'Loop has no executable blocks inside. Add or enable at least one block in the loop.'
|
||||
const loopType = loopConfig.loopType || 'for'
|
||||
logger.error(errorMessage, { loopId })
|
||||
await this.addLoopErrorLog(ctx, loopId, loopType, errorMessage, {})
|
||||
const errorScope: LoopScope = {
|
||||
iteration: 0,
|
||||
maxIterations: 0,
|
||||
loopType,
|
||||
currentIterationOutputs: new Map(),
|
||||
allIterationOutputs: [],
|
||||
condition: 'false',
|
||||
validationError: errorMessage,
|
||||
}
|
||||
if (!ctx.loopExecutions) {
|
||||
ctx.loopExecutions = new Map()
|
||||
}
|
||||
ctx.loopExecutions.set(loopId, errorScope)
|
||||
throw new Error(errorMessage)
|
||||
}
|
||||
|
||||
const scope: LoopScope = {
|
||||
iteration: 0,
|
||||
currentIterationOutputs: new Map(),
|
||||
@@ -93,6 +116,24 @@ export class LoopOrchestrator {
|
||||
|
||||
case 'forEach': {
|
||||
scope.loopType = 'forEach'
|
||||
if (
|
||||
loopConfig.forEachItems === undefined ||
|
||||
loopConfig.forEachItems === null ||
|
||||
loopConfig.forEachItems === ''
|
||||
) {
|
||||
const errorMessage =
|
||||
'ForEach loop collection is empty. Provide an array or a reference that resolves to a collection.'
|
||||
logger.error(errorMessage, { loopId })
|
||||
await this.addLoopErrorLog(ctx, loopId, loopType, errorMessage, {
|
||||
forEachItems: loopConfig.forEachItems,
|
||||
})
|
||||
scope.items = []
|
||||
scope.maxIterations = 0
|
||||
scope.validationError = errorMessage
|
||||
scope.condition = buildLoopIndexCondition(0)
|
||||
ctx.loopExecutions?.set(loopId, scope)
|
||||
throw new Error(errorMessage)
|
||||
}
|
||||
let items: any[]
|
||||
try {
|
||||
items = resolveArrayInput(ctx, loopConfig.forEachItems, this.resolver)
|
||||
|
||||
@@ -57,6 +57,15 @@ export class ParallelOrchestrator {
|
||||
throw new Error(`Parallel config not found: ${parallelId}`)
|
||||
}
|
||||
|
||||
if (terminalNodesCount === 0 || parallelConfig.nodes.length === 0) {
|
||||
const errorMessage =
|
||||
'Parallel has no executable blocks inside. Add or enable at least one block in the parallel.'
|
||||
logger.error(errorMessage, { parallelId })
|
||||
await this.addParallelErrorLog(ctx, parallelId, errorMessage, {})
|
||||
this.setErrorScope(ctx, parallelId, errorMessage)
|
||||
throw new Error(errorMessage)
|
||||
}
|
||||
|
||||
let items: any[] | undefined
|
||||
let branchCount: number
|
||||
let isEmpty = false
|
||||
@@ -67,7 +76,10 @@ export class ParallelOrchestrator {
|
||||
items = resolved.items
|
||||
isEmpty = resolved.isEmpty ?? false
|
||||
} catch (error) {
|
||||
const errorMessage = `Parallel Items did not resolve: ${error instanceof Error ? error.message : String(error)}`
|
||||
const baseErrorMessage = error instanceof Error ? error.message : String(error)
|
||||
const errorMessage = baseErrorMessage.startsWith('Parallel collection distribution is empty')
|
||||
? baseErrorMessage
|
||||
: `Parallel Items did not resolve: ${baseErrorMessage}`
|
||||
logger.error(errorMessage, { parallelId, distribution: parallelConfig.distribution })
|
||||
await this.addParallelErrorLog(ctx, parallelId, errorMessage, {
|
||||
distribution: parallelConfig.distribution,
|
||||
@@ -258,7 +270,9 @@ export class ParallelOrchestrator {
|
||||
config.distribution === null ||
|
||||
config.distribution === ''
|
||||
) {
|
||||
return []
|
||||
throw new Error(
|
||||
'Parallel collection distribution is empty. Provide an array or a reference that resolves to a collection.'
|
||||
)
|
||||
}
|
||||
return resolveArrayInput(ctx, config.distribution, this.resolver)
|
||||
}
|
||||
|
||||
@@ -211,6 +211,16 @@ export const auth = betterAuth({
|
||||
modifiedAccount.refreshTokenExpiresAt = getMicrosoftRefreshTokenExpiry()
|
||||
}
|
||||
|
||||
// Box token response does not include a scope field, so Better Auth
|
||||
// stores nothing. Populate it from the requested scopes so the
|
||||
// credential-selector can verify permissions.
|
||||
if (account.providerId === 'box' && !account.scope) {
|
||||
const requestedScopes = getCanonicalScopesForProvider('box')
|
||||
if (requestedScopes.length > 0) {
|
||||
modifiedAccount.scope = requestedScopes.join(' ')
|
||||
}
|
||||
}
|
||||
|
||||
return { data: modifiedAccount }
|
||||
},
|
||||
after: async (account) => {
|
||||
@@ -478,6 +488,7 @@ export const auth = betterAuth({
|
||||
'sharepoint',
|
||||
'jira',
|
||||
'airtable',
|
||||
'box',
|
||||
'dropbox',
|
||||
'salesforce',
|
||||
'wealthbox',
|
||||
@@ -488,6 +499,7 @@ export const auth = betterAuth({
|
||||
'shopify',
|
||||
'trello',
|
||||
'calcom',
|
||||
'docusign',
|
||||
...SSO_TRUSTED_PROVIDERS,
|
||||
],
|
||||
},
|
||||
@@ -2176,6 +2188,51 @@ export const auth = betterAuth({
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
providerId: 'box',
|
||||
clientId: env.BOX_CLIENT_ID as string,
|
||||
clientSecret: env.BOX_CLIENT_SECRET as string,
|
||||
authorizationUrl: 'https://account.box.com/api/oauth2/authorize',
|
||||
tokenUrl: 'https://api.box.com/oauth2/token',
|
||||
scopes: getCanonicalScopesForProvider('box'),
|
||||
responseType: 'code',
|
||||
redirectURI: `${getBaseUrl()}/api/auth/oauth2/callback/box`,
|
||||
getUserInfo: async (tokens) => {
|
||||
try {
|
||||
const response = await fetch('https://api.box.com/2.0/users/me', {
|
||||
headers: {
|
||||
Authorization: `Bearer ${tokens.accessToken}`,
|
||||
},
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text()
|
||||
logger.error('Box API error:', {
|
||||
status: response.status,
|
||||
statusText: response.statusText,
|
||||
body: errorText,
|
||||
})
|
||||
throw new Error(`Box API error: ${response.status} ${response.statusText}`)
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
|
||||
return {
|
||||
id: `${data.id}-${crypto.randomUUID()}`,
|
||||
email: data.login,
|
||||
name: data.name || data.login,
|
||||
emailVerified: true,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
image: data.avatar_url || undefined,
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error('Error in Box getUserInfo:', error)
|
||||
throw error
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
providerId: 'dropbox',
|
||||
clientId: env.DROPBOX_CLIENT_ID as string,
|
||||
@@ -2593,6 +2650,64 @@ export const auth = betterAuth({
|
||||
},
|
||||
},
|
||||
|
||||
// DocuSign provider
|
||||
{
|
||||
providerId: 'docusign',
|
||||
clientId: env.DOCUSIGN_CLIENT_ID as string,
|
||||
clientSecret: env.DOCUSIGN_CLIENT_SECRET as string,
|
||||
authorizationUrl: 'https://account-d.docusign.com/oauth/auth',
|
||||
tokenUrl: 'https://account-d.docusign.com/oauth/token',
|
||||
userInfoUrl: 'https://account-d.docusign.com/oauth/userinfo',
|
||||
scopes: getCanonicalScopesForProvider('docusign'),
|
||||
responseType: 'code',
|
||||
accessType: 'offline',
|
||||
prompt: 'consent',
|
||||
redirectURI: `${getBaseUrl()}/api/auth/oauth2/callback/docusign`,
|
||||
getUserInfo: async (tokens) => {
|
||||
try {
|
||||
logger.info('Fetching DocuSign user profile')
|
||||
|
||||
const response = await fetch('https://account-d.docusign.com/oauth/userinfo', {
|
||||
headers: {
|
||||
Authorization: `Bearer ${tokens.accessToken}`,
|
||||
},
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
await response.text().catch(() => {})
|
||||
logger.error('Failed to fetch DocuSign user info', {
|
||||
status: response.status,
|
||||
statusText: response.statusText,
|
||||
})
|
||||
throw new Error('Failed to fetch user info')
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
const accounts = data.accounts ?? []
|
||||
const defaultAccount =
|
||||
accounts.find((a: { is_default: boolean }) => a.is_default) ?? accounts[0]
|
||||
const accountName = defaultAccount?.account_name || 'DocuSign Account'
|
||||
|
||||
if (data.scope) {
|
||||
tokens.scopes = data.scope.split(/\s+/).filter(Boolean)
|
||||
}
|
||||
|
||||
return {
|
||||
id: `${data.sub}-${crypto.randomUUID()}`,
|
||||
name: data.name || accountName,
|
||||
email: data.email || `${data.sub}@docusign.com`,
|
||||
emailVerified: true,
|
||||
image: undefined,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error('Error in DocuSign getUserInfo:', { error })
|
||||
return null
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
// Cal.com provider
|
||||
{
|
||||
providerId: 'calcom',
|
||||
|
||||
@@ -503,22 +503,37 @@ export async function handleInvoicePaymentSucceeded(event: Stripe.Event) {
|
||||
wasBlocked = row.length > 0 ? !!row[0].blocked : false
|
||||
}
|
||||
|
||||
if (isOrgPlan(sub.plan)) {
|
||||
await unblockOrgMembers(sub.referenceId, 'payment_failed')
|
||||
} else {
|
||||
// Only unblock users blocked for payment_failed, not disputes
|
||||
await db
|
||||
.update(userStats)
|
||||
.set({ billingBlocked: false, billingBlockedReason: null })
|
||||
.where(
|
||||
and(
|
||||
eq(userStats.userId, sub.referenceId),
|
||||
eq(userStats.billingBlockedReason, 'payment_failed')
|
||||
// For proration invoices (mid-cycle upgrades/seat changes), only unblock if real money
|
||||
// was collected. A $0 credit invoice from a downgrade should not unblock a user who
|
||||
// was blocked for a different failed payment.
|
||||
const isProrationInvoice = invoice.billing_reason === 'subscription_update'
|
||||
const shouldUnblock = !isProrationInvoice || (invoice.amount_paid ?? 0) > 0
|
||||
|
||||
if (shouldUnblock) {
|
||||
if (isOrgPlan(sub.plan)) {
|
||||
await unblockOrgMembers(sub.referenceId, 'payment_failed')
|
||||
} else {
|
||||
await db
|
||||
.update(userStats)
|
||||
.set({ billingBlocked: false, billingBlockedReason: null })
|
||||
.where(
|
||||
and(
|
||||
eq(userStats.userId, sub.referenceId),
|
||||
eq(userStats.billingBlockedReason, 'payment_failed')
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
} else {
|
||||
logger.info('Skipping unblock for zero-amount proration invoice', {
|
||||
invoiceId: invoice.id,
|
||||
billingReason: invoice.billing_reason,
|
||||
amountPaid: invoice.amount_paid,
|
||||
})
|
||||
}
|
||||
|
||||
if (wasBlocked) {
|
||||
// Only reset usage for cycle renewals — proration invoices should not wipe
|
||||
// accumulated usage mid-cycle.
|
||||
if (wasBlocked && !isProrationInvoice) {
|
||||
await resetUsageForSubscription({ plan: sub.plan, referenceId: sub.referenceId })
|
||||
}
|
||||
} catch (error) {
|
||||
@@ -584,14 +599,6 @@ export async function handleInvoicePaymentFailed(event: Stripe.Event) {
|
||||
|
||||
// Block users after first payment failure
|
||||
if (attemptCount >= 1) {
|
||||
logger.error('Payment failure - blocking users', {
|
||||
invoiceId: invoice.id,
|
||||
customerId,
|
||||
attemptCount,
|
||||
isOverageInvoice,
|
||||
stripeSubscriptionId,
|
||||
})
|
||||
|
||||
const records = await db
|
||||
.select()
|
||||
.from(subscriptionTable)
|
||||
@@ -600,6 +607,15 @@ export async function handleInvoicePaymentFailed(event: Stripe.Event) {
|
||||
|
||||
if (records.length > 0) {
|
||||
const sub = records[0]
|
||||
|
||||
logger.error('Payment failure - blocking users', {
|
||||
invoiceId: invoice.id,
|
||||
customerId,
|
||||
attemptCount,
|
||||
isOverageInvoice,
|
||||
stripeSubscriptionId,
|
||||
})
|
||||
|
||||
if (isOrgPlan(sub.plan)) {
|
||||
const memberCount = await blockOrgMembers(sub.referenceId, 'payment_failed')
|
||||
logger.info('Blocked team/enterprise members due to payment failure', {
|
||||
|
||||
@@ -263,6 +263,8 @@ export const env = createEnv({
|
||||
NOTION_CLIENT_SECRET: z.string().optional(), // Notion OAuth client secret
|
||||
DISCORD_CLIENT_ID: z.string().optional(), // Discord OAuth client ID
|
||||
DISCORD_CLIENT_SECRET: z.string().optional(), // Discord OAuth client secret
|
||||
DOCUSIGN_CLIENT_ID: z.string().optional(), // DocuSign OAuth client ID
|
||||
DOCUSIGN_CLIENT_SECRET: z.string().optional(), // DocuSign OAuth client secret
|
||||
MICROSOFT_CLIENT_ID: z.string().optional(), // Microsoft OAuth client ID for Office 365/Teams
|
||||
MICROSOFT_CLIENT_SECRET: z.string().optional(), // Microsoft OAuth client secret
|
||||
HUBSPOT_CLIENT_ID: z.string().optional(), // HubSpot OAuth client ID
|
||||
@@ -275,6 +277,8 @@ export const env = createEnv({
|
||||
PIPEDRIVE_CLIENT_SECRET: z.string().optional(), // Pipedrive OAuth client secret
|
||||
LINEAR_CLIENT_ID: z.string().optional(), // Linear OAuth client ID
|
||||
LINEAR_CLIENT_SECRET: z.string().optional(), // Linear OAuth client secret
|
||||
BOX_CLIENT_ID: z.string().optional(), // Box OAuth client ID
|
||||
BOX_CLIENT_SECRET: z.string().optional(), // Box OAuth client secret
|
||||
DROPBOX_CLIENT_ID: z.string().optional(), // Dropbox OAuth client ID
|
||||
DROPBOX_CLIENT_SECRET: z.string().optional(), // Dropbox OAuth client secret
|
||||
SLACK_CLIENT_ID: z.string().optional(), // Slack OAuth client ID
|
||||
|
||||
@@ -3,8 +3,10 @@ import {
|
||||
AirtableIcon,
|
||||
AsanaIcon,
|
||||
AttioIcon,
|
||||
BoxCompanyIcon,
|
||||
CalComIcon,
|
||||
ConfluenceIcon,
|
||||
DocuSignIcon,
|
||||
DropboxIcon,
|
||||
GmailIcon,
|
||||
GoogleAdsIcon,
|
||||
@@ -571,6 +573,21 @@ export const OAUTH_PROVIDERS: Record<string, OAuthProviderConfig> = {
|
||||
},
|
||||
defaultService: 'linear',
|
||||
},
|
||||
box: {
|
||||
name: 'Box',
|
||||
icon: BoxCompanyIcon,
|
||||
services: {
|
||||
box: {
|
||||
name: 'Box',
|
||||
description: 'Manage files, folders, and e-signatures with Box.',
|
||||
providerId: 'box',
|
||||
icon: BoxCompanyIcon,
|
||||
baseProviderIcon: BoxCompanyIcon,
|
||||
scopes: ['root_readwrite', 'sign_requests.readwrite'],
|
||||
},
|
||||
},
|
||||
defaultService: 'box',
|
||||
},
|
||||
dropbox: {
|
||||
name: 'Dropbox',
|
||||
icon: DropboxIcon,
|
||||
@@ -779,6 +796,21 @@ export const OAUTH_PROVIDERS: Record<string, OAuthProviderConfig> = {
|
||||
},
|
||||
defaultService: 'calcom',
|
||||
},
|
||||
docusign: {
|
||||
name: 'DocuSign',
|
||||
icon: DocuSignIcon,
|
||||
services: {
|
||||
docusign: {
|
||||
name: 'DocuSign',
|
||||
description: 'Send documents for e-signature with DocuSign.',
|
||||
providerId: 'docusign',
|
||||
icon: DocuSignIcon,
|
||||
baseProviderIcon: DocuSignIcon,
|
||||
scopes: ['signature', 'extended'],
|
||||
},
|
||||
},
|
||||
defaultService: 'docusign',
|
||||
},
|
||||
pipedrive: {
|
||||
name: 'Pipedrive',
|
||||
icon: PipedriveIcon,
|
||||
@@ -1109,6 +1141,28 @@ function getProviderAuthConfig(provider: string): ProviderAuthConfig {
|
||||
useBasicAuth: false,
|
||||
}
|
||||
}
|
||||
case 'box': {
|
||||
const { clientId, clientSecret } = getCredentials(env.BOX_CLIENT_ID, env.BOX_CLIENT_SECRET)
|
||||
return {
|
||||
tokenEndpoint: 'https://api.box.com/oauth2/token',
|
||||
clientId,
|
||||
clientSecret,
|
||||
useBasicAuth: false,
|
||||
}
|
||||
}
|
||||
case 'docusign': {
|
||||
const { clientId, clientSecret } = getCredentials(
|
||||
env.DOCUSIGN_CLIENT_ID,
|
||||
env.DOCUSIGN_CLIENT_SECRET
|
||||
)
|
||||
return {
|
||||
tokenEndpoint: 'https://account-d.docusign.com/oauth/token',
|
||||
clientId,
|
||||
clientSecret,
|
||||
useBasicAuth: true,
|
||||
supportsRefreshTokenRotation: false,
|
||||
}
|
||||
}
|
||||
case 'dropbox': {
|
||||
const { clientId, clientSecret } = getCredentials(
|
||||
env.DROPBOX_CLIENT_ID,
|
||||
|
||||
@@ -21,6 +21,7 @@ export type OAuthProvider =
|
||||
| 'airtable'
|
||||
| 'notion'
|
||||
| 'jira'
|
||||
| 'box'
|
||||
| 'dropbox'
|
||||
| 'microsoft'
|
||||
| 'microsoft-dataverse'
|
||||
@@ -47,6 +48,7 @@ export type OAuthProvider =
|
||||
| 'wordpress'
|
||||
| 'spotify'
|
||||
| 'calcom'
|
||||
| 'docusign'
|
||||
|
||||
export type OAuthService =
|
||||
| 'google'
|
||||
@@ -69,6 +71,7 @@ export type OAuthService =
|
||||
| 'airtable'
|
||||
| 'notion'
|
||||
| 'jira'
|
||||
| 'box'
|
||||
| 'dropbox'
|
||||
| 'microsoft-dataverse'
|
||||
| 'microsoft-excel'
|
||||
@@ -94,6 +97,7 @@ export type OAuthService =
|
||||
| 'wordpress'
|
||||
| 'spotify'
|
||||
| 'calcom'
|
||||
| 'docusign'
|
||||
| 'github'
|
||||
|
||||
export interface OAuthProviderConfig {
|
||||
|
||||
@@ -342,6 +342,7 @@ export const SCOPE_DESCRIPTIONS: Record<string, string> = {
|
||||
// Box scopes
|
||||
root_readwrite: 'Read and write all files and folders in Box account',
|
||||
root_readonly: 'Read all files and folders in Box account',
|
||||
'sign_requests.readwrite': 'Create and manage Box Sign e-signature requests',
|
||||
|
||||
// Shopify scopes
|
||||
write_products: 'Read and manage Shopify products',
|
||||
@@ -395,6 +396,10 @@ export const SCOPE_DESCRIPTIONS: Record<string, string> = {
|
||||
'user-read-playback-position': 'View playback position in podcasts',
|
||||
'ugc-image-upload': 'Upload images to Spotify playlists',
|
||||
|
||||
// DocuSign scopes
|
||||
signature: 'Create and send envelopes for e-signature',
|
||||
extended: 'Extended access to DocuSign account features',
|
||||
|
||||
// Attio scopes
|
||||
'record_permission:read-write': 'Read and write CRM records',
|
||||
'object_configuration:read-write': 'Read and manage object schemas',
|
||||
|
||||
@@ -23,6 +23,10 @@ export const SUBBLOCK_ID_MIGRATIONS: Record<string, Record<string, string>> = {
|
||||
knowledge: {
|
||||
knowledgeBaseId: 'knowledgeBaseSelector',
|
||||
},
|
||||
ashby: {
|
||||
emailType: '_removed_emailType',
|
||||
phoneType: '_removed_phoneType',
|
||||
},
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -796,6 +796,7 @@ describe('Schedule Deploy Utilities', () => {
|
||||
|
||||
expect(mockOnConflictDoUpdate).toHaveBeenCalledWith({
|
||||
target: expect.any(Array),
|
||||
targetWhere: expect.objectContaining({ type: 'isNull' }),
|
||||
set: expect.objectContaining({
|
||||
blockId: 'block-1',
|
||||
cronExpression: '0 9 * * *',
|
||||
|
||||
@@ -138,6 +138,7 @@ export async function createSchedulesForDeploy(
|
||||
workflowSchedule.blockId,
|
||||
workflowSchedule.deploymentVersionId,
|
||||
],
|
||||
targetWhere: isNull(workflowSchedule.archivedAt),
|
||||
set: setValues,
|
||||
})
|
||||
|
||||
|
||||
@@ -159,6 +159,7 @@
|
||||
"resend": "^4.1.2",
|
||||
"rss-parser": "3.13.0",
|
||||
"sharp": "0.34.3",
|
||||
"soap": "1.8.0",
|
||||
"socket.io": "^4.8.1",
|
||||
"socket.io-client": "4.8.1",
|
||||
"ssh2": "^1.17.0",
|
||||
|
||||
76
apps/sim/tools/ashby/add_candidate_tag.ts
Normal file
76
apps/sim/tools/ashby/add_candidate_tag.ts
Normal file
@@ -0,0 +1,76 @@
|
||||
import type { ToolConfig, ToolResponse } from '@/tools/types'
|
||||
|
||||
interface AshbyAddCandidateTagParams {
|
||||
apiKey: string
|
||||
candidateId: string
|
||||
tagId: string
|
||||
}
|
||||
|
||||
interface AshbyAddCandidateTagResponse extends ToolResponse {
|
||||
output: {
|
||||
success: boolean
|
||||
}
|
||||
}
|
||||
|
||||
export const addCandidateTagTool: ToolConfig<
|
||||
AshbyAddCandidateTagParams,
|
||||
AshbyAddCandidateTagResponse
|
||||
> = {
|
||||
id: 'ashby_add_candidate_tag',
|
||||
name: 'Ashby Add Candidate Tag',
|
||||
description: 'Adds a tag to a candidate in Ashby.',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
apiKey: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Ashby API Key',
|
||||
},
|
||||
candidateId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'The UUID of the candidate to add the tag to',
|
||||
},
|
||||
tagId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'The UUID of the tag to add',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: 'https://api.ashbyhq.com/candidate.addTag',
|
||||
method: 'POST',
|
||||
headers: (params) => ({
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Basic ${btoa(`${params.apiKey}:`)}`,
|
||||
}),
|
||||
body: (params) => ({
|
||||
candidateId: params.candidateId,
|
||||
tagId: params.tagId,
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
const data = await response.json()
|
||||
|
||||
if (!data.success) {
|
||||
throw new Error(data.errorInfo?.message || 'Failed to add tag to candidate')
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
success: true,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
success: { type: 'boolean', description: 'Whether the tag was successfully added' },
|
||||
},
|
||||
}
|
||||
94
apps/sim/tools/ashby/change_application_stage.ts
Normal file
94
apps/sim/tools/ashby/change_application_stage.ts
Normal file
@@ -0,0 +1,94 @@
|
||||
import type { ToolConfig, ToolResponse } from '@/tools/types'
|
||||
|
||||
interface AshbyChangeApplicationStageParams {
|
||||
apiKey: string
|
||||
applicationId: string
|
||||
interviewStageId: string
|
||||
archiveReasonId?: string
|
||||
}
|
||||
|
||||
interface AshbyChangeApplicationStageResponse extends ToolResponse {
|
||||
output: {
|
||||
applicationId: string
|
||||
stageId: string | null
|
||||
}
|
||||
}
|
||||
|
||||
export const changeApplicationStageTool: ToolConfig<
|
||||
AshbyChangeApplicationStageParams,
|
||||
AshbyChangeApplicationStageResponse
|
||||
> = {
|
||||
id: 'ashby_change_application_stage',
|
||||
name: 'Ashby Change Application Stage',
|
||||
description:
|
||||
'Moves an application to a different interview stage. Requires an archive reason when moving to an Archived stage.',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
apiKey: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Ashby API Key',
|
||||
},
|
||||
applicationId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'The UUID of the application to update the stage of',
|
||||
},
|
||||
interviewStageId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'The UUID of the interview stage to move the application to',
|
||||
},
|
||||
archiveReasonId: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description:
|
||||
'Archive reason UUID. Required when moving to an Archived stage, ignored otherwise',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: 'https://api.ashbyhq.com/application.changeStage',
|
||||
method: 'POST',
|
||||
headers: (params) => ({
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Basic ${btoa(`${params.apiKey}:`)}`,
|
||||
}),
|
||||
body: (params) => {
|
||||
const body: Record<string, unknown> = {
|
||||
applicationId: params.applicationId,
|
||||
interviewStageId: params.interviewStageId,
|
||||
}
|
||||
if (params.archiveReasonId) body.archiveReasonId = params.archiveReasonId
|
||||
return body
|
||||
},
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
const data = await response.json()
|
||||
|
||||
if (!data.success) {
|
||||
throw new Error(data.errorInfo?.message || 'Failed to change application stage')
|
||||
}
|
||||
|
||||
const r = data.results
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
applicationId: r.id ?? null,
|
||||
stageId: r.currentInterviewStage?.id ?? null,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
applicationId: { type: 'string', description: 'Application UUID' },
|
||||
stageId: { type: 'string', description: 'New interview stage UUID' },
|
||||
},
|
||||
}
|
||||
@@ -13,27 +13,7 @@ interface AshbyCreateApplicationParams {
|
||||
|
||||
interface AshbyCreateApplicationResponse extends ToolResponse {
|
||||
output: {
|
||||
id: string
|
||||
status: string
|
||||
candidate: {
|
||||
id: string
|
||||
name: string
|
||||
}
|
||||
job: {
|
||||
id: string
|
||||
title: string
|
||||
}
|
||||
currentInterviewStage: {
|
||||
id: string
|
||||
title: string
|
||||
type: string
|
||||
} | null
|
||||
source: {
|
||||
id: string
|
||||
title: string
|
||||
} | null
|
||||
createdAt: string
|
||||
updatedAt: string
|
||||
applicationId: string
|
||||
}
|
||||
}
|
||||
|
||||
@@ -132,74 +112,12 @@ export const createApplicationTool: ToolConfig<
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
id: r.id ?? null,
|
||||
status: r.status ?? null,
|
||||
candidate: {
|
||||
id: r.candidate?.id ?? null,
|
||||
name: r.candidate?.name ?? null,
|
||||
},
|
||||
job: {
|
||||
id: r.job?.id ?? null,
|
||||
title: r.job?.title ?? null,
|
||||
},
|
||||
currentInterviewStage: r.currentInterviewStage
|
||||
? {
|
||||
id: r.currentInterviewStage.id ?? null,
|
||||
title: r.currentInterviewStage.title ?? null,
|
||||
type: r.currentInterviewStage.type ?? null,
|
||||
}
|
||||
: null,
|
||||
source: r.source
|
||||
? {
|
||||
id: r.source.id ?? null,
|
||||
title: r.source.title ?? null,
|
||||
}
|
||||
: null,
|
||||
createdAt: r.createdAt ?? null,
|
||||
updatedAt: r.updatedAt ?? null,
|
||||
applicationId: r.applicationId ?? null,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
id: { type: 'string', description: 'Created application UUID' },
|
||||
status: { type: 'string', description: 'Application status (Active, Hired, Archived, Lead)' },
|
||||
candidate: {
|
||||
type: 'object',
|
||||
description: 'Associated candidate',
|
||||
properties: {
|
||||
id: { type: 'string', description: 'Candidate UUID' },
|
||||
name: { type: 'string', description: 'Candidate name' },
|
||||
},
|
||||
},
|
||||
job: {
|
||||
type: 'object',
|
||||
description: 'Associated job',
|
||||
properties: {
|
||||
id: { type: 'string', description: 'Job UUID' },
|
||||
title: { type: 'string', description: 'Job title' },
|
||||
},
|
||||
},
|
||||
currentInterviewStage: {
|
||||
type: 'object',
|
||||
description: 'Current interview stage',
|
||||
optional: true,
|
||||
properties: {
|
||||
id: { type: 'string', description: 'Stage UUID' },
|
||||
title: { type: 'string', description: 'Stage title' },
|
||||
type: { type: 'string', description: 'Stage type' },
|
||||
},
|
||||
},
|
||||
source: {
|
||||
type: 'object',
|
||||
description: 'Application source',
|
||||
optional: true,
|
||||
properties: {
|
||||
id: { type: 'string', description: 'Source UUID' },
|
||||
title: { type: 'string', description: 'Source title' },
|
||||
},
|
||||
},
|
||||
createdAt: { type: 'string', description: 'ISO 8601 creation timestamp' },
|
||||
updatedAt: { type: 'string', description: 'ISO 8601 last update timestamp' },
|
||||
applicationId: { type: 'string', description: 'Created application UUID' },
|
||||
},
|
||||
}
|
||||
|
||||
@@ -25,28 +25,16 @@ export const createCandidateTool: ToolConfig<
|
||||
},
|
||||
email: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Primary email address for the candidate',
|
||||
},
|
||||
emailType: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Email address type: Personal, Work, or Other (default Work)',
|
||||
},
|
||||
phoneNumber: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Primary phone number for the candidate',
|
||||
},
|
||||
phoneType: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Phone number type: Personal, Work, or Other (default Work)',
|
||||
},
|
||||
linkedInUrl: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
@@ -77,27 +65,11 @@ export const createCandidateTool: ToolConfig<
|
||||
body: (params) => {
|
||||
const body: Record<string, unknown> = {
|
||||
name: params.name,
|
||||
email: params.email,
|
||||
}
|
||||
if (params.email) {
|
||||
body.primaryEmailAddress = {
|
||||
value: params.email,
|
||||
type: params.emailType || 'Work',
|
||||
isPrimary: true,
|
||||
}
|
||||
}
|
||||
if (params.phoneNumber) {
|
||||
body.primaryPhoneNumber = {
|
||||
value: params.phoneNumber,
|
||||
type: params.phoneType || 'Work',
|
||||
isPrimary: true,
|
||||
}
|
||||
}
|
||||
if (params.linkedInUrl || params.githubUrl) {
|
||||
const socialLinks: Array<{ url: string; type: string }> = []
|
||||
if (params.linkedInUrl) socialLinks.push({ url: params.linkedInUrl, type: 'LinkedIn' })
|
||||
if (params.githubUrl) socialLinks.push({ url: params.githubUrl, type: 'GitHub' })
|
||||
body.socialLinks = socialLinks
|
||||
}
|
||||
if (params.phoneNumber) body.phoneNumber = params.phoneNumber
|
||||
if (params.linkedInUrl) body.linkedInUrl = params.linkedInUrl
|
||||
if (params.githubUrl) body.githubUrl = params.githubUrl
|
||||
if (params.sourceId) body.sourceId = params.sourceId
|
||||
return body
|
||||
},
|
||||
|
||||
@@ -78,35 +78,12 @@ export const createNoteTool: ToolConfig<AshbyCreateNoteParams, AshbyCreateNoteRe
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
id: r.id ?? null,
|
||||
content: r.content ?? null,
|
||||
author: r.author
|
||||
? {
|
||||
id: r.author.id ?? null,
|
||||
firstName: r.author.firstName ?? null,
|
||||
lastName: r.author.lastName ?? null,
|
||||
email: r.author.email ?? null,
|
||||
}
|
||||
: null,
|
||||
createdAt: r.createdAt ?? null,
|
||||
noteId: r.id ?? null,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
id: { type: 'string', description: 'Created note UUID' },
|
||||
content: { type: 'string', description: 'Note content as stored' },
|
||||
author: {
|
||||
type: 'object',
|
||||
description: 'Note author',
|
||||
optional: true,
|
||||
properties: {
|
||||
id: { type: 'string', description: 'Author user UUID' },
|
||||
firstName: { type: 'string', description: 'First name' },
|
||||
lastName: { type: 'string', description: 'Last name' },
|
||||
email: { type: 'string', description: 'Email address' },
|
||||
},
|
||||
},
|
||||
createdAt: { type: 'string', description: 'ISO 8601 creation timestamp' },
|
||||
noteId: { type: 'string', description: 'Created note UUID' },
|
||||
},
|
||||
}
|
||||
|
||||
@@ -30,7 +30,7 @@ export const getCandidateTool: ToolConfig<AshbyGetCandidateParams, AshbyGetCandi
|
||||
Authorization: `Basic ${btoa(`${params.apiKey}:`)}`,
|
||||
}),
|
||||
body: (params) => ({
|
||||
candidateId: params.candidateId,
|
||||
candidateId: params.candidateId.trim(),
|
||||
}),
|
||||
},
|
||||
|
||||
|
||||
@@ -30,7 +30,7 @@ export const getJobTool: ToolConfig<AshbyGetJobParams, AshbyGetJobResponse> = {
|
||||
Authorization: `Basic ${btoa(`${params.apiKey}:`)}`,
|
||||
}),
|
||||
body: (params) => ({
|
||||
jobId: params.jobId,
|
||||
jobId: params.jobId.trim(),
|
||||
}),
|
||||
},
|
||||
|
||||
@@ -63,7 +63,7 @@ export const getJobTool: ToolConfig<AshbyGetJobParams, AshbyGetJobResponse> = {
|
||||
outputs: {
|
||||
id: { type: 'string', description: 'Job UUID' },
|
||||
title: { type: 'string', description: 'Job title' },
|
||||
status: { type: 'string', description: 'Job status (Open, Closed, Draft, Archived, On Hold)' },
|
||||
status: { type: 'string', description: 'Job status (Open, Closed, Draft, Archived)' },
|
||||
employmentType: {
|
||||
type: 'string',
|
||||
description: 'Employment type (FullTime, PartTime, Intern, Contract, Temporary)',
|
||||
|
||||
106
apps/sim/tools/ashby/get_job_posting.ts
Normal file
106
apps/sim/tools/ashby/get_job_posting.ts
Normal file
@@ -0,0 +1,106 @@
|
||||
import type { ToolConfig, ToolResponse } from '@/tools/types'
|
||||
|
||||
interface AshbyGetJobPostingParams {
|
||||
apiKey: string
|
||||
jobPostingId: string
|
||||
}
|
||||
|
||||
interface AshbyGetJobPostingResponse extends ToolResponse {
|
||||
output: {
|
||||
id: string
|
||||
title: string
|
||||
jobId: string | null
|
||||
locationName: string | null
|
||||
departmentName: string | null
|
||||
employmentType: string | null
|
||||
descriptionPlain: string | null
|
||||
isListed: boolean
|
||||
publishedDate: string | null
|
||||
externalLink: string | null
|
||||
}
|
||||
}
|
||||
|
||||
export const getJobPostingTool: ToolConfig<AshbyGetJobPostingParams, AshbyGetJobPostingResponse> = {
|
||||
id: 'ashby_get_job_posting',
|
||||
name: 'Ashby Get Job Posting',
|
||||
description: 'Retrieves full details about a single job posting by its ID.',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
apiKey: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Ashby API Key',
|
||||
},
|
||||
jobPostingId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'The UUID of the job posting to fetch',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: 'https://api.ashbyhq.com/jobPosting.info',
|
||||
method: 'POST',
|
||||
headers: (params) => ({
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Basic ${btoa(`${params.apiKey}:`)}`,
|
||||
}),
|
||||
body: (params) => ({
|
||||
jobPostingId: params.jobPostingId,
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
const data = await response.json()
|
||||
|
||||
if (!data.success) {
|
||||
throw new Error(data.errorInfo?.message || 'Failed to get job posting')
|
||||
}
|
||||
|
||||
const r = data.results
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
id: r.id ?? null,
|
||||
title: r.jobTitle ?? r.title ?? null,
|
||||
jobId: r.jobId ?? null,
|
||||
locationName: r.locationName ?? null,
|
||||
departmentName: r.departmentName ?? null,
|
||||
employmentType: r.employmentType ?? null,
|
||||
descriptionPlain: r.descriptionPlain ?? r.description ?? null,
|
||||
isListed: r.isListed ?? false,
|
||||
publishedDate: r.publishedDate ?? null,
|
||||
externalLink: r.externalLink ?? null,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
id: { type: 'string', description: 'Job posting UUID' },
|
||||
title: { type: 'string', description: 'Job posting title' },
|
||||
jobId: { type: 'string', description: 'Associated job UUID', optional: true },
|
||||
locationName: { type: 'string', description: 'Location name', optional: true },
|
||||
departmentName: { type: 'string', description: 'Department name', optional: true },
|
||||
employmentType: {
|
||||
type: 'string',
|
||||
description: 'Employment type (e.g. FullTime, PartTime, Contract)',
|
||||
optional: true,
|
||||
},
|
||||
descriptionPlain: {
|
||||
type: 'string',
|
||||
description: 'Job posting description in plain text',
|
||||
optional: true,
|
||||
},
|
||||
isListed: { type: 'boolean', description: 'Whether the posting is publicly listed' },
|
||||
publishedDate: { type: 'string', description: 'ISO 8601 published date', optional: true },
|
||||
externalLink: {
|
||||
type: 'string',
|
||||
description: 'External link to the job posting',
|
||||
optional: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
116
apps/sim/tools/ashby/get_offer.ts
Normal file
116
apps/sim/tools/ashby/get_offer.ts
Normal file
@@ -0,0 +1,116 @@
|
||||
import type { ToolConfig, ToolResponse } from '@/tools/types'
|
||||
|
||||
interface AshbyGetOfferParams {
|
||||
apiKey: string
|
||||
offerId: string
|
||||
}
|
||||
|
||||
interface AshbyGetOfferResponse extends ToolResponse {
|
||||
output: {
|
||||
id: string
|
||||
offerStatus: string
|
||||
acceptanceStatus: string | null
|
||||
applicationId: string | null
|
||||
startDate: string | null
|
||||
salary: {
|
||||
currencyCode: string
|
||||
value: number
|
||||
} | null
|
||||
openingId: string | null
|
||||
createdAt: string | null
|
||||
}
|
||||
}
|
||||
|
||||
export const getOfferTool: ToolConfig<AshbyGetOfferParams, AshbyGetOfferResponse> = {
|
||||
id: 'ashby_get_offer',
|
||||
name: 'Ashby Get Offer',
|
||||
description: 'Retrieves full details about a single offer by its ID.',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
apiKey: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Ashby API Key',
|
||||
},
|
||||
offerId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'The UUID of the offer to fetch',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: 'https://api.ashbyhq.com/offer.info',
|
||||
method: 'POST',
|
||||
headers: (params) => ({
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Basic ${btoa(`${params.apiKey}:`)}`,
|
||||
}),
|
||||
body: (params) => ({
|
||||
offerId: params.offerId,
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
const data = await response.json()
|
||||
|
||||
if (!data.success) {
|
||||
throw new Error(data.errorInfo?.message || 'Failed to get offer')
|
||||
}
|
||||
|
||||
const r = data.results
|
||||
const v = r.latestVersion
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
id: r.id ?? null,
|
||||
offerStatus: r.offerStatus ?? null,
|
||||
acceptanceStatus: r.acceptanceStatus ?? null,
|
||||
applicationId: r.applicationId ?? null,
|
||||
startDate: v?.startDate ?? null,
|
||||
salary: v?.salary
|
||||
? {
|
||||
currencyCode: v.salary.currencyCode ?? null,
|
||||
value: v.salary.value ?? null,
|
||||
}
|
||||
: null,
|
||||
openingId: v?.openingId ?? null,
|
||||
createdAt: v?.createdAt ?? null,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
id: { type: 'string', description: 'Offer UUID' },
|
||||
offerStatus: {
|
||||
type: 'string',
|
||||
description: 'Offer status (e.g. WaitingOnCandidateResponse, CandidateAccepted)',
|
||||
},
|
||||
acceptanceStatus: {
|
||||
type: 'string',
|
||||
description: 'Acceptance status (e.g. Accepted, Declined, Pending)',
|
||||
optional: true,
|
||||
},
|
||||
applicationId: { type: 'string', description: 'Associated application UUID', optional: true },
|
||||
startDate: { type: 'string', description: 'Offer start date', optional: true },
|
||||
salary: {
|
||||
type: 'object',
|
||||
description: 'Salary details',
|
||||
optional: true,
|
||||
properties: {
|
||||
currencyCode: { type: 'string', description: 'ISO 4217 currency code' },
|
||||
value: { type: 'number', description: 'Salary amount' },
|
||||
},
|
||||
},
|
||||
openingId: { type: 'string', description: 'Associated opening UUID', optional: true },
|
||||
createdAt: {
|
||||
type: 'string',
|
||||
description: 'ISO 8601 creation timestamp (from latest version)',
|
||||
optional: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -1,28 +1,58 @@
|
||||
import { addCandidateTagTool } from '@/tools/ashby/add_candidate_tag'
|
||||
import { changeApplicationStageTool } from '@/tools/ashby/change_application_stage'
|
||||
import { createApplicationTool } from '@/tools/ashby/create_application'
|
||||
import { createCandidateTool } from '@/tools/ashby/create_candidate'
|
||||
import { createNoteTool } from '@/tools/ashby/create_note'
|
||||
import { getApplicationTool } from '@/tools/ashby/get_application'
|
||||
import { getCandidateTool } from '@/tools/ashby/get_candidate'
|
||||
import { getJobTool } from '@/tools/ashby/get_job'
|
||||
import { getJobPostingTool } from '@/tools/ashby/get_job_posting'
|
||||
import { getOfferTool } from '@/tools/ashby/get_offer'
|
||||
import { listApplicationsTool } from '@/tools/ashby/list_applications'
|
||||
import { listArchiveReasonsTool } from '@/tools/ashby/list_archive_reasons'
|
||||
import { listCandidateTagsTool } from '@/tools/ashby/list_candidate_tags'
|
||||
import { listCandidatesTool } from '@/tools/ashby/list_candidates'
|
||||
import { listCustomFieldsTool } from '@/tools/ashby/list_custom_fields'
|
||||
import { listDepartmentsTool } from '@/tools/ashby/list_departments'
|
||||
import { listInterviewsTool } from '@/tools/ashby/list_interviews'
|
||||
import { listJobPostingsTool } from '@/tools/ashby/list_job_postings'
|
||||
import { listJobsTool } from '@/tools/ashby/list_jobs'
|
||||
import { listLocationsTool } from '@/tools/ashby/list_locations'
|
||||
import { listNotesTool } from '@/tools/ashby/list_notes'
|
||||
import { listOffersTool } from '@/tools/ashby/list_offers'
|
||||
import { listOpeningsTool } from '@/tools/ashby/list_openings'
|
||||
import { listSourcesTool } from '@/tools/ashby/list_sources'
|
||||
import { listUsersTool } from '@/tools/ashby/list_users'
|
||||
import { removeCandidateTagTool } from '@/tools/ashby/remove_candidate_tag'
|
||||
import { searchCandidatesTool } from '@/tools/ashby/search_candidates'
|
||||
import { updateCandidateTool } from '@/tools/ashby/update_candidate'
|
||||
|
||||
export const ashbyAddCandidateTagTool = addCandidateTagTool
|
||||
export const ashbyChangeApplicationStageTool = changeApplicationStageTool
|
||||
export const ashbyCreateApplicationTool = createApplicationTool
|
||||
export const ashbyCreateCandidateTool = createCandidateTool
|
||||
export const ashbyCreateNoteTool = createNoteTool
|
||||
export const ashbyGetApplicationTool = getApplicationTool
|
||||
export const ashbyGetCandidateTool = getCandidateTool
|
||||
export const ashbyGetJobTool = getJobTool
|
||||
export const ashbyGetJobPostingTool = getJobPostingTool
|
||||
export const ashbyGetOfferTool = getOfferTool
|
||||
export const ashbyListApplicationsTool = listApplicationsTool
|
||||
export const ashbyListArchiveReasonsTool = listArchiveReasonsTool
|
||||
export const ashbyListCandidateTagsTool = listCandidateTagsTool
|
||||
export const ashbyListCandidatesTool = listCandidatesTool
|
||||
export const ashbyListCustomFieldsTool = listCustomFieldsTool
|
||||
export const ashbyListDepartmentsTool = listDepartmentsTool
|
||||
export const ashbyListInterviewsTool = listInterviewsTool
|
||||
export const ashbyListJobPostingsTool = listJobPostingsTool
|
||||
export const ashbyListJobsTool = listJobsTool
|
||||
export const ashbyListLocationsTool = listLocationsTool
|
||||
export const ashbyListNotesTool = listNotesTool
|
||||
export const ashbyListOffersTool = listOffersTool
|
||||
export const ashbyListOpeningsTool = listOpeningsTool
|
||||
export const ashbyListSourcesTool = listSourcesTool
|
||||
export const ashbyListUsersTool = listUsersTool
|
||||
export const ashbyRemoveCandidateTagTool = removeCandidateTagTool
|
||||
export const ashbySearchCandidatesTool = searchCandidatesTool
|
||||
export const ashbyUpdateCandidateTool = updateCandidateTool
|
||||
|
||||
|
||||
@@ -71,7 +71,7 @@ export const listApplicationsTool: ToolConfig<
|
||||
if (params.status) body.status = [params.status]
|
||||
if (params.jobId) body.jobId = params.jobId
|
||||
if (params.candidateId) body.candidateId = params.candidateId
|
||||
if (params.createdAfter) body.createdAfter = params.createdAfter
|
||||
if (params.createdAfter) body.createdAfter = new Date(params.createdAfter).getTime()
|
||||
return body
|
||||
},
|
||||
},
|
||||
|
||||
81
apps/sim/tools/ashby/list_archive_reasons.ts
Normal file
81
apps/sim/tools/ashby/list_archive_reasons.ts
Normal file
@@ -0,0 +1,81 @@
|
||||
import type { ToolConfig, ToolResponse } from '@/tools/types'
|
||||
|
||||
interface AshbyListArchiveReasonsParams {
|
||||
apiKey: string
|
||||
}
|
||||
|
||||
interface AshbyListArchiveReasonsResponse extends ToolResponse {
|
||||
output: {
|
||||
archiveReasons: Array<{
|
||||
id: string
|
||||
text: string
|
||||
reasonType: string
|
||||
isArchived: boolean
|
||||
}>
|
||||
}
|
||||
}
|
||||
|
||||
export const listArchiveReasonsTool: ToolConfig<
|
||||
AshbyListArchiveReasonsParams,
|
||||
AshbyListArchiveReasonsResponse
|
||||
> = {
|
||||
id: 'ashby_list_archive_reasons',
|
||||
name: 'Ashby List Archive Reasons',
|
||||
description: 'Lists all archive reasons configured in Ashby.',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
apiKey: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Ashby API Key',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: 'https://api.ashbyhq.com/archiveReason.list',
|
||||
method: 'POST',
|
||||
headers: (params) => ({
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Basic ${btoa(`${params.apiKey}:`)}`,
|
||||
}),
|
||||
body: () => ({}),
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
const data = await response.json()
|
||||
|
||||
if (!data.success) {
|
||||
throw new Error(data.errorInfo?.message || 'Failed to list archive reasons')
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
archiveReasons: (data.results ?? []).map((r: Record<string, unknown>) => ({
|
||||
id: r.id ?? null,
|
||||
text: r.text ?? null,
|
||||
reasonType: r.reasonType ?? null,
|
||||
isArchived: r.isArchived ?? false,
|
||||
})),
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
archiveReasons: {
|
||||
type: 'array',
|
||||
description: 'List of archive reasons',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
id: { type: 'string', description: 'Archive reason UUID' },
|
||||
text: { type: 'string', description: 'Archive reason text' },
|
||||
reasonType: { type: 'string', description: 'Reason type' },
|
||||
isArchived: { type: 'boolean', description: 'Whether the reason is archived' },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
78
apps/sim/tools/ashby/list_candidate_tags.ts
Normal file
78
apps/sim/tools/ashby/list_candidate_tags.ts
Normal file
@@ -0,0 +1,78 @@
|
||||
import type { ToolConfig, ToolResponse } from '@/tools/types'
|
||||
|
||||
interface AshbyListCandidateTagsParams {
|
||||
apiKey: string
|
||||
}
|
||||
|
||||
interface AshbyListCandidateTagsResponse extends ToolResponse {
|
||||
output: {
|
||||
tags: Array<{
|
||||
id: string
|
||||
title: string
|
||||
isArchived: boolean
|
||||
}>
|
||||
}
|
||||
}
|
||||
|
||||
export const listCandidateTagsTool: ToolConfig<
|
||||
AshbyListCandidateTagsParams,
|
||||
AshbyListCandidateTagsResponse
|
||||
> = {
|
||||
id: 'ashby_list_candidate_tags',
|
||||
name: 'Ashby List Candidate Tags',
|
||||
description: 'Lists all candidate tags configured in Ashby.',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
apiKey: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Ashby API Key',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: 'https://api.ashbyhq.com/candidateTag.list',
|
||||
method: 'POST',
|
||||
headers: (params) => ({
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Basic ${btoa(`${params.apiKey}:`)}`,
|
||||
}),
|
||||
body: () => ({}),
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
const data = await response.json()
|
||||
|
||||
if (!data.success) {
|
||||
throw new Error(data.errorInfo?.message || 'Failed to list candidate tags')
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
tags: (data.results ?? []).map((t: Record<string, unknown>) => ({
|
||||
id: t.id ?? null,
|
||||
title: t.title ?? null,
|
||||
isArchived: t.isArchived ?? false,
|
||||
})),
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
tags: {
|
||||
type: 'array',
|
||||
description: 'List of candidate tags',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
id: { type: 'string', description: 'Tag UUID' },
|
||||
title: { type: 'string', description: 'Tag title' },
|
||||
isArchived: { type: 'boolean', description: 'Whether the tag is archived' },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
87
apps/sim/tools/ashby/list_custom_fields.ts
Normal file
87
apps/sim/tools/ashby/list_custom_fields.ts
Normal file
@@ -0,0 +1,87 @@
|
||||
import type { ToolConfig, ToolResponse } from '@/tools/types'
|
||||
|
||||
interface AshbyListCustomFieldsParams {
|
||||
apiKey: string
|
||||
}
|
||||
|
||||
interface AshbyListCustomFieldsResponse extends ToolResponse {
|
||||
output: {
|
||||
customFields: Array<{
|
||||
id: string
|
||||
title: string
|
||||
fieldType: string
|
||||
objectType: string
|
||||
isArchived: boolean
|
||||
}>
|
||||
}
|
||||
}
|
||||
|
||||
export const listCustomFieldsTool: ToolConfig<
|
||||
AshbyListCustomFieldsParams,
|
||||
AshbyListCustomFieldsResponse
|
||||
> = {
|
||||
id: 'ashby_list_custom_fields',
|
||||
name: 'Ashby List Custom Fields',
|
||||
description: 'Lists all custom field definitions configured in Ashby.',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
apiKey: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Ashby API Key',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: 'https://api.ashbyhq.com/customField.list',
|
||||
method: 'POST',
|
||||
headers: (params) => ({
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Basic ${btoa(`${params.apiKey}:`)}`,
|
||||
}),
|
||||
body: () => ({}),
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
const data = await response.json()
|
||||
|
||||
if (!data.success) {
|
||||
throw new Error(data.errorInfo?.message || 'Failed to list custom fields')
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
customFields: (data.results ?? []).map((f: Record<string, unknown>) => ({
|
||||
id: f.id ?? null,
|
||||
title: f.title ?? null,
|
||||
fieldType: f.fieldType ?? null,
|
||||
objectType: f.objectType ?? null,
|
||||
isArchived: f.isArchived ?? false,
|
||||
})),
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
customFields: {
|
||||
type: 'array',
|
||||
description: 'List of custom field definitions',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
id: { type: 'string', description: 'Custom field UUID' },
|
||||
title: { type: 'string', description: 'Custom field title' },
|
||||
fieldType: { type: 'string', description: 'Field type (e.g. String, Number, Boolean)' },
|
||||
objectType: {
|
||||
type: 'string',
|
||||
description: 'Object type the field applies to (e.g. Candidate, Application, Job)',
|
||||
},
|
||||
isArchived: { type: 'boolean', description: 'Whether the custom field is archived' },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
85
apps/sim/tools/ashby/list_departments.ts
Normal file
85
apps/sim/tools/ashby/list_departments.ts
Normal file
@@ -0,0 +1,85 @@
|
||||
import type { ToolConfig, ToolResponse } from '@/tools/types'
|
||||
|
||||
interface AshbyListDepartmentsParams {
|
||||
apiKey: string
|
||||
}
|
||||
|
||||
interface AshbyListDepartmentsResponse extends ToolResponse {
|
||||
output: {
|
||||
departments: Array<{
|
||||
id: string
|
||||
name: string
|
||||
isArchived: boolean
|
||||
parentId: string | null
|
||||
}>
|
||||
}
|
||||
}
|
||||
|
||||
export const listDepartmentsTool: ToolConfig<
|
||||
AshbyListDepartmentsParams,
|
||||
AshbyListDepartmentsResponse
|
||||
> = {
|
||||
id: 'ashby_list_departments',
|
||||
name: 'Ashby List Departments',
|
||||
description: 'Lists all departments in Ashby.',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
apiKey: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Ashby API Key',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: 'https://api.ashbyhq.com/department.list',
|
||||
method: 'POST',
|
||||
headers: (params) => ({
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Basic ${btoa(`${params.apiKey}:`)}`,
|
||||
}),
|
||||
body: () => ({}),
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
const data = await response.json()
|
||||
|
||||
if (!data.success) {
|
||||
throw new Error(data.errorInfo?.message || 'Failed to list departments')
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
departments: (data.results ?? []).map((d: Record<string, unknown>) => ({
|
||||
id: d.id ?? null,
|
||||
name: d.name ?? null,
|
||||
isArchived: d.isArchived ?? false,
|
||||
parentId: (d.parentId as string) ?? null,
|
||||
})),
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
departments: {
|
||||
type: 'array',
|
||||
description: 'List of departments',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
id: { type: 'string', description: 'Department UUID' },
|
||||
name: { type: 'string', description: 'Department name' },
|
||||
isArchived: { type: 'boolean', description: 'Whether the department is archived' },
|
||||
parentId: {
|
||||
type: 'string',
|
||||
description: 'Parent department UUID',
|
||||
optional: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
137
apps/sim/tools/ashby/list_interviews.ts
Normal file
137
apps/sim/tools/ashby/list_interviews.ts
Normal file
@@ -0,0 +1,137 @@
|
||||
import type { ToolConfig, ToolResponse } from '@/tools/types'
|
||||
|
||||
interface AshbyListInterviewSchedulesParams {
|
||||
apiKey: string
|
||||
applicationId?: string
|
||||
interviewStageId?: string
|
||||
cursor?: string
|
||||
perPage?: number
|
||||
}
|
||||
|
||||
interface AshbyListInterviewSchedulesResponse extends ToolResponse {
|
||||
output: {
|
||||
interviewSchedules: Array<{
|
||||
id: string
|
||||
applicationId: string
|
||||
interviewStageId: string | null
|
||||
status: string | null
|
||||
createdAt: string
|
||||
}>
|
||||
moreDataAvailable: boolean
|
||||
nextCursor: string | null
|
||||
}
|
||||
}
|
||||
|
||||
export const listInterviewsTool: ToolConfig<
|
||||
AshbyListInterviewSchedulesParams,
|
||||
AshbyListInterviewSchedulesResponse
|
||||
> = {
|
||||
id: 'ashby_list_interviews',
|
||||
name: 'Ashby List Interview Schedules',
|
||||
description:
|
||||
'Lists interview schedules in Ashby, optionally filtered by application or interview stage.',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
apiKey: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Ashby API Key',
|
||||
},
|
||||
applicationId: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'The UUID of the application to list interview schedules for',
|
||||
},
|
||||
interviewStageId: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'The UUID of the interview stage to list interview schedules for',
|
||||
},
|
||||
cursor: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Opaque pagination cursor from a previous response nextCursor value',
|
||||
},
|
||||
perPage: {
|
||||
type: 'number',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Number of results per page (default 100)',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: 'https://api.ashbyhq.com/interviewSchedule.list',
|
||||
method: 'POST',
|
||||
headers: (params) => ({
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Basic ${btoa(`${params.apiKey}:`)}`,
|
||||
}),
|
||||
body: (params) => {
|
||||
const body: Record<string, unknown> = {}
|
||||
if (params.applicationId) body.applicationId = params.applicationId
|
||||
if (params.interviewStageId) body.interviewStageId = params.interviewStageId
|
||||
if (params.cursor) body.cursor = params.cursor
|
||||
if (params.perPage) body.limit = params.perPage
|
||||
return body
|
||||
},
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
const data = await response.json()
|
||||
|
||||
if (!data.success) {
|
||||
throw new Error(data.errorInfo?.message || 'Failed to list interview schedules')
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
interviewSchedules: (data.results ?? []).map((s: Record<string, unknown>) => ({
|
||||
id: s.id ?? null,
|
||||
applicationId: s.applicationId ?? null,
|
||||
interviewStageId: s.interviewStageId ?? null,
|
||||
status: s.status ?? null,
|
||||
createdAt: s.createdAt ?? null,
|
||||
})),
|
||||
moreDataAvailable: data.moreDataAvailable ?? false,
|
||||
nextCursor: data.nextCursor ?? null,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
interviewSchedules: {
|
||||
type: 'array',
|
||||
description: 'List of interview schedules',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
id: { type: 'string', description: 'Interview schedule UUID' },
|
||||
applicationId: { type: 'string', description: 'Associated application UUID' },
|
||||
interviewStageId: {
|
||||
type: 'string',
|
||||
description: 'Interview stage UUID',
|
||||
optional: true,
|
||||
},
|
||||
status: { type: 'string', description: 'Schedule status', optional: true },
|
||||
createdAt: { type: 'string', description: 'ISO 8601 creation timestamp' },
|
||||
},
|
||||
},
|
||||
},
|
||||
moreDataAvailable: {
|
||||
type: 'boolean',
|
||||
description: 'Whether more pages of results exist',
|
||||
},
|
||||
nextCursor: {
|
||||
type: 'string',
|
||||
description: 'Opaque cursor for fetching the next page',
|
||||
optional: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
97
apps/sim/tools/ashby/list_job_postings.ts
Normal file
97
apps/sim/tools/ashby/list_job_postings.ts
Normal file
@@ -0,0 +1,97 @@
|
||||
import type { ToolConfig, ToolResponse } from '@/tools/types'
|
||||
|
||||
interface AshbyListJobPostingsParams {
|
||||
apiKey: string
|
||||
}
|
||||
|
||||
interface AshbyListJobPostingsResponse extends ToolResponse {
|
||||
output: {
|
||||
jobPostings: Array<{
|
||||
id: string
|
||||
title: string
|
||||
jobId: string | null
|
||||
locationName: string | null
|
||||
departmentName: string | null
|
||||
employmentType: string | null
|
||||
isListed: boolean
|
||||
publishedDate: string | null
|
||||
}>
|
||||
}
|
||||
}
|
||||
|
||||
export const listJobPostingsTool: ToolConfig<
|
||||
AshbyListJobPostingsParams,
|
||||
AshbyListJobPostingsResponse
|
||||
> = {
|
||||
id: 'ashby_list_job_postings',
|
||||
name: 'Ashby List Job Postings',
|
||||
description: 'Lists all job postings in Ashby.',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
apiKey: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Ashby API Key',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: 'https://api.ashbyhq.com/jobPosting.list',
|
||||
method: 'POST',
|
||||
headers: (params) => ({
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Basic ${btoa(`${params.apiKey}:`)}`,
|
||||
}),
|
||||
body: () => ({}),
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
const data = await response.json()
|
||||
|
||||
if (!data.success) {
|
||||
throw new Error(data.errorInfo?.message || 'Failed to list job postings')
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
jobPostings: (data.results ?? []).map((jp: Record<string, unknown>) => ({
|
||||
id: jp.id ?? null,
|
||||
title: (jp.jobTitle as string) ?? (jp.title as string) ?? null,
|
||||
jobId: jp.jobId ?? null,
|
||||
locationName: jp.locationName ?? null,
|
||||
departmentName: jp.departmentName ?? null,
|
||||
employmentType: jp.employmentType ?? null,
|
||||
isListed: jp.isListed ?? false,
|
||||
publishedDate: jp.publishedDate ?? null,
|
||||
})),
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
jobPostings: {
|
||||
type: 'array',
|
||||
description: 'List of job postings',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
id: { type: 'string', description: 'Job posting UUID' },
|
||||
title: { type: 'string', description: 'Job posting title' },
|
||||
jobId: { type: 'string', description: 'Associated job UUID', optional: true },
|
||||
locationName: { type: 'string', description: 'Location name', optional: true },
|
||||
departmentName: { type: 'string', description: 'Department name', optional: true },
|
||||
employmentType: {
|
||||
type: 'string',
|
||||
description: 'Employment type (e.g. FullTime, PartTime, Contract)',
|
||||
optional: true,
|
||||
},
|
||||
isListed: { type: 'boolean', description: 'Whether the posting is publicly listed' },
|
||||
publishedDate: { type: 'string', description: 'ISO 8601 published date', optional: true },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
112
apps/sim/tools/ashby/list_locations.ts
Normal file
112
apps/sim/tools/ashby/list_locations.ts
Normal file
@@ -0,0 +1,112 @@
|
||||
import type { ToolConfig, ToolResponse } from '@/tools/types'
|
||||
|
||||
interface AshbyListLocationsParams {
|
||||
apiKey: string
|
||||
}
|
||||
|
||||
interface AshbyListLocationsResponse extends ToolResponse {
|
||||
output: {
|
||||
locations: Array<{
|
||||
id: string
|
||||
name: string
|
||||
isArchived: boolean
|
||||
isRemote: boolean
|
||||
address: {
|
||||
city: string | null
|
||||
region: string | null
|
||||
country: string | null
|
||||
} | null
|
||||
}>
|
||||
}
|
||||
}
|
||||
|
||||
export const listLocationsTool: ToolConfig<AshbyListLocationsParams, AshbyListLocationsResponse> = {
|
||||
id: 'ashby_list_locations',
|
||||
name: 'Ashby List Locations',
|
||||
description: 'Lists all locations configured in Ashby.',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
apiKey: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Ashby API Key',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: 'https://api.ashbyhq.com/location.list',
|
||||
method: 'POST',
|
||||
headers: (params) => ({
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Basic ${btoa(`${params.apiKey}:`)}`,
|
||||
}),
|
||||
body: () => ({}),
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
const data = await response.json()
|
||||
|
||||
if (!data.success) {
|
||||
throw new Error(data.errorInfo?.message || 'Failed to list locations')
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
locations: (data.results ?? []).map(
|
||||
(
|
||||
l: Record<string, unknown> & {
|
||||
address?: {
|
||||
postalAddress?: {
|
||||
addressLocality?: string
|
||||
addressRegion?: string
|
||||
addressCountry?: string
|
||||
}
|
||||
}
|
||||
}
|
||||
) => ({
|
||||
id: l.id ?? null,
|
||||
name: l.name ?? null,
|
||||
isArchived: l.isArchived ?? false,
|
||||
isRemote: l.isRemote ?? false,
|
||||
address: l.address?.postalAddress
|
||||
? {
|
||||
city: l.address.postalAddress.addressLocality ?? null,
|
||||
region: l.address.postalAddress.addressRegion ?? null,
|
||||
country: l.address.postalAddress.addressCountry ?? null,
|
||||
}
|
||||
: null,
|
||||
})
|
||||
),
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
locations: {
|
||||
type: 'array',
|
||||
description: 'List of locations',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
id: { type: 'string', description: 'Location UUID' },
|
||||
name: { type: 'string', description: 'Location name' },
|
||||
isArchived: { type: 'boolean', description: 'Whether the location is archived' },
|
||||
isRemote: { type: 'boolean', description: 'Whether this is a remote location' },
|
||||
address: {
|
||||
type: 'object',
|
||||
description: 'Location address',
|
||||
optional: true,
|
||||
properties: {
|
||||
city: { type: 'string', description: 'City', optional: true },
|
||||
region: { type: 'string', description: 'State or region', optional: true },
|
||||
country: { type: 'string', description: 'Country', optional: true },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -10,17 +10,16 @@ interface AshbyListOffersResponse extends ToolResponse {
|
||||
output: {
|
||||
offers: Array<{
|
||||
id: string
|
||||
status: string
|
||||
candidate: {
|
||||
id: string
|
||||
name: string
|
||||
offerStatus: string
|
||||
acceptanceStatus: string | null
|
||||
applicationId: string | null
|
||||
startDate: string | null
|
||||
salary: {
|
||||
currencyCode: string
|
||||
value: number
|
||||
} | null
|
||||
job: {
|
||||
id: string
|
||||
title: string
|
||||
} | null
|
||||
createdAt: string
|
||||
updatedAt: string
|
||||
openingId: string | null
|
||||
createdAt: string | null
|
||||
}>
|
||||
moreDataAvailable: boolean
|
||||
nextCursor: string | null
|
||||
@@ -82,27 +81,31 @@ export const listOffersTool: ToolConfig<AshbyListOffersParams, AshbyListOffersRe
|
||||
offers: (data.results ?? []).map(
|
||||
(
|
||||
o: Record<string, unknown> & {
|
||||
candidate?: { id?: string; name?: string }
|
||||
job?: { id?: string; title?: string }
|
||||
latestVersion?: {
|
||||
startDate?: string
|
||||
salary?: { currencyCode?: string; value?: number }
|
||||
openingId?: string
|
||||
createdAt?: string
|
||||
}
|
||||
}
|
||||
) => ({
|
||||
id: o.id ?? null,
|
||||
status: o.status ?? o.offerStatus ?? null,
|
||||
candidate: o.candidate
|
||||
? {
|
||||
id: o.candidate.id ?? null,
|
||||
name: o.candidate.name ?? null,
|
||||
}
|
||||
: null,
|
||||
job: o.job
|
||||
? {
|
||||
id: o.job.id ?? null,
|
||||
title: o.job.title ?? null,
|
||||
}
|
||||
: null,
|
||||
createdAt: o.createdAt ?? null,
|
||||
updatedAt: o.updatedAt ?? null,
|
||||
})
|
||||
) => {
|
||||
const v = o.latestVersion
|
||||
return {
|
||||
id: o.id ?? null,
|
||||
offerStatus: o.offerStatus ?? null,
|
||||
acceptanceStatus: o.acceptanceStatus ?? null,
|
||||
applicationId: o.applicationId ?? null,
|
||||
startDate: v?.startDate ?? null,
|
||||
salary: v?.salary
|
||||
? {
|
||||
currencyCode: v.salary.currencyCode ?? null,
|
||||
value: v.salary.value ?? null,
|
||||
}
|
||||
: null,
|
||||
openingId: v?.openingId ?? null,
|
||||
createdAt: v?.createdAt ?? null,
|
||||
}
|
||||
}
|
||||
),
|
||||
moreDataAvailable: data.moreDataAvailable ?? false,
|
||||
nextCursor: data.nextCursor ?? null,
|
||||
@@ -118,27 +121,25 @@ export const listOffersTool: ToolConfig<AshbyListOffersParams, AshbyListOffersRe
|
||||
type: 'object',
|
||||
properties: {
|
||||
id: { type: 'string', description: 'Offer UUID' },
|
||||
status: { type: 'string', description: 'Offer status' },
|
||||
candidate: {
|
||||
offerStatus: { type: 'string', description: 'Offer status' },
|
||||
acceptanceStatus: { type: 'string', description: 'Acceptance status', optional: true },
|
||||
applicationId: {
|
||||
type: 'string',
|
||||
description: 'Associated application UUID',
|
||||
optional: true,
|
||||
},
|
||||
startDate: { type: 'string', description: 'Offer start date', optional: true },
|
||||
salary: {
|
||||
type: 'object',
|
||||
description: 'Associated candidate',
|
||||
description: 'Salary details',
|
||||
optional: true,
|
||||
properties: {
|
||||
id: { type: 'string', description: 'Candidate UUID' },
|
||||
name: { type: 'string', description: 'Candidate name' },
|
||||
currencyCode: { type: 'string', description: 'ISO 4217 currency code' },
|
||||
value: { type: 'number', description: 'Salary amount' },
|
||||
},
|
||||
},
|
||||
job: {
|
||||
type: 'object',
|
||||
description: 'Associated job',
|
||||
optional: true,
|
||||
properties: {
|
||||
id: { type: 'string', description: 'Job UUID' },
|
||||
title: { type: 'string', description: 'Job title' },
|
||||
},
|
||||
},
|
||||
createdAt: { type: 'string', description: 'ISO 8601 creation timestamp' },
|
||||
updatedAt: { type: 'string', description: 'ISO 8601 last update timestamp' },
|
||||
openingId: { type: 'string', description: 'Associated opening UUID', optional: true },
|
||||
createdAt: { type: 'string', description: 'ISO 8601 creation timestamp', optional: true },
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
117
apps/sim/tools/ashby/list_openings.ts
Normal file
117
apps/sim/tools/ashby/list_openings.ts
Normal file
@@ -0,0 +1,117 @@
|
||||
import type { ToolConfig, ToolResponse } from '@/tools/types'
|
||||
|
||||
interface AshbyListOpeningsParams {
|
||||
apiKey: string
|
||||
cursor?: string
|
||||
perPage?: number
|
||||
}
|
||||
|
||||
interface AshbyListOpeningsResponse extends ToolResponse {
|
||||
output: {
|
||||
openings: Array<{
|
||||
id: string
|
||||
openingState: string | null
|
||||
isArchived: boolean
|
||||
openedAt: string | null
|
||||
closedAt: string | null
|
||||
}>
|
||||
moreDataAvailable: boolean
|
||||
nextCursor: string | null
|
||||
}
|
||||
}
|
||||
|
||||
export const listOpeningsTool: ToolConfig<AshbyListOpeningsParams, AshbyListOpeningsResponse> = {
|
||||
id: 'ashby_list_openings',
|
||||
name: 'Ashby List Openings',
|
||||
description: 'Lists all openings in Ashby with pagination.',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
apiKey: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Ashby API Key',
|
||||
},
|
||||
cursor: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Opaque pagination cursor from a previous response nextCursor value',
|
||||
},
|
||||
perPage: {
|
||||
type: 'number',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Number of results per page (default 100)',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: 'https://api.ashbyhq.com/opening.list',
|
||||
method: 'POST',
|
||||
headers: (params) => ({
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Basic ${btoa(`${params.apiKey}:`)}`,
|
||||
}),
|
||||
body: (params) => {
|
||||
const body: Record<string, unknown> = {}
|
||||
if (params.cursor) body.cursor = params.cursor
|
||||
if (params.perPage) body.limit = params.perPage
|
||||
return body
|
||||
},
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
const data = await response.json()
|
||||
|
||||
if (!data.success) {
|
||||
throw new Error(data.errorInfo?.message || 'Failed to list openings')
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
openings: (data.results ?? []).map((o: Record<string, unknown>) => ({
|
||||
id: o.id ?? null,
|
||||
openingState: o.openingState ?? null,
|
||||
isArchived: o.isArchived ?? false,
|
||||
openedAt: o.openedAt ?? null,
|
||||
closedAt: o.closedAt ?? null,
|
||||
})),
|
||||
moreDataAvailable: data.moreDataAvailable ?? false,
|
||||
nextCursor: data.nextCursor ?? null,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
openings: {
|
||||
type: 'array',
|
||||
description: 'List of openings',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
id: { type: 'string', description: 'Opening UUID' },
|
||||
openingState: {
|
||||
type: 'string',
|
||||
description: 'Opening state (Approved, Closed, Draft, Filled, Open)',
|
||||
optional: true,
|
||||
},
|
||||
isArchived: { type: 'boolean', description: 'Whether the opening is archived' },
|
||||
openedAt: { type: 'string', description: 'ISO 8601 opened timestamp', optional: true },
|
||||
closedAt: { type: 'string', description: 'ISO 8601 closed timestamp', optional: true },
|
||||
},
|
||||
},
|
||||
},
|
||||
moreDataAvailable: {
|
||||
type: 'boolean',
|
||||
description: 'Whether more pages of results exist',
|
||||
},
|
||||
nextCursor: {
|
||||
type: 'string',
|
||||
description: 'Opaque cursor for fetching the next page',
|
||||
optional: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
75
apps/sim/tools/ashby/list_sources.ts
Normal file
75
apps/sim/tools/ashby/list_sources.ts
Normal file
@@ -0,0 +1,75 @@
|
||||
import type { ToolConfig, ToolResponse } from '@/tools/types'
|
||||
|
||||
interface AshbyListSourcesParams {
|
||||
apiKey: string
|
||||
}
|
||||
|
||||
interface AshbyListSourcesResponse extends ToolResponse {
|
||||
output: {
|
||||
sources: Array<{
|
||||
id: string
|
||||
title: string
|
||||
isArchived: boolean
|
||||
}>
|
||||
}
|
||||
}
|
||||
|
||||
export const listSourcesTool: ToolConfig<AshbyListSourcesParams, AshbyListSourcesResponse> = {
|
||||
id: 'ashby_list_sources',
|
||||
name: 'Ashby List Sources',
|
||||
description: 'Lists all candidate sources configured in Ashby.',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
apiKey: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Ashby API Key',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: 'https://api.ashbyhq.com/source.list',
|
||||
method: 'POST',
|
||||
headers: (params) => ({
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Basic ${btoa(`${params.apiKey}:`)}`,
|
||||
}),
|
||||
body: () => ({}),
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
const data = await response.json()
|
||||
|
||||
if (!data.success) {
|
||||
throw new Error(data.errorInfo?.message || 'Failed to list sources')
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
sources: (data.results ?? []).map((s: Record<string, unknown>) => ({
|
||||
id: s.id ?? null,
|
||||
title: s.title ?? null,
|
||||
isArchived: s.isArchived ?? false,
|
||||
})),
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
sources: {
|
||||
type: 'array',
|
||||
description: 'List of sources',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
id: { type: 'string', description: 'Source UUID' },
|
||||
title: { type: 'string', description: 'Source title' },
|
||||
isArchived: { type: 'boolean', description: 'Whether the source is archived' },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
121
apps/sim/tools/ashby/list_users.ts
Normal file
121
apps/sim/tools/ashby/list_users.ts
Normal file
@@ -0,0 +1,121 @@
|
||||
import type { ToolConfig, ToolResponse } from '@/tools/types'
|
||||
|
||||
interface AshbyListUsersParams {
|
||||
apiKey: string
|
||||
cursor?: string
|
||||
perPage?: number
|
||||
}
|
||||
|
||||
interface AshbyListUsersResponse extends ToolResponse {
|
||||
output: {
|
||||
users: Array<{
|
||||
id: string
|
||||
firstName: string
|
||||
lastName: string
|
||||
email: string
|
||||
isEnabled: boolean
|
||||
globalRole: string | null
|
||||
}>
|
||||
moreDataAvailable: boolean
|
||||
nextCursor: string | null
|
||||
}
|
||||
}
|
||||
|
||||
export const listUsersTool: ToolConfig<AshbyListUsersParams, AshbyListUsersResponse> = {
|
||||
id: 'ashby_list_users',
|
||||
name: 'Ashby List Users',
|
||||
description: 'Lists all users in Ashby with pagination.',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
apiKey: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Ashby API Key',
|
||||
},
|
||||
cursor: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Opaque pagination cursor from a previous response nextCursor value',
|
||||
},
|
||||
perPage: {
|
||||
type: 'number',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Number of results per page (default 100)',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: 'https://api.ashbyhq.com/user.list',
|
||||
method: 'POST',
|
||||
headers: (params) => ({
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Basic ${btoa(`${params.apiKey}:`)}`,
|
||||
}),
|
||||
body: (params) => {
|
||||
const body: Record<string, unknown> = {}
|
||||
if (params.cursor) body.cursor = params.cursor
|
||||
if (params.perPage) body.limit = params.perPage
|
||||
return body
|
||||
},
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
const data = await response.json()
|
||||
|
||||
if (!data.success) {
|
||||
throw new Error(data.errorInfo?.message || 'Failed to list users')
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
users: (data.results ?? []).map((u: Record<string, unknown>) => ({
|
||||
id: u.id ?? null,
|
||||
firstName: u.firstName ?? null,
|
||||
lastName: u.lastName ?? null,
|
||||
email: u.email ?? null,
|
||||
isEnabled: u.isEnabled ?? false,
|
||||
globalRole: u.globalRole ?? null,
|
||||
})),
|
||||
moreDataAvailable: data.moreDataAvailable ?? false,
|
||||
nextCursor: data.nextCursor ?? null,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
users: {
|
||||
type: 'array',
|
||||
description: 'List of users',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
id: { type: 'string', description: 'User UUID' },
|
||||
firstName: { type: 'string', description: 'First name' },
|
||||
lastName: { type: 'string', description: 'Last name' },
|
||||
email: { type: 'string', description: 'Email address' },
|
||||
isEnabled: { type: 'boolean', description: 'Whether the user account is enabled' },
|
||||
globalRole: {
|
||||
type: 'string',
|
||||
description:
|
||||
'User role (Organization Admin, Elevated Access, Limited Access, External Recruiter)',
|
||||
optional: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
moreDataAvailable: {
|
||||
type: 'boolean',
|
||||
description: 'Whether more pages of results exist',
|
||||
},
|
||||
nextCursor: {
|
||||
type: 'string',
|
||||
description: 'Opaque cursor for fetching the next page',
|
||||
optional: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
76
apps/sim/tools/ashby/remove_candidate_tag.ts
Normal file
76
apps/sim/tools/ashby/remove_candidate_tag.ts
Normal file
@@ -0,0 +1,76 @@
|
||||
import type { ToolConfig, ToolResponse } from '@/tools/types'
|
||||
|
||||
interface AshbyRemoveCandidateTagParams {
|
||||
apiKey: string
|
||||
candidateId: string
|
||||
tagId: string
|
||||
}
|
||||
|
||||
interface AshbyRemoveCandidateTagResponse extends ToolResponse {
|
||||
output: {
|
||||
success: boolean
|
||||
}
|
||||
}
|
||||
|
||||
export const removeCandidateTagTool: ToolConfig<
|
||||
AshbyRemoveCandidateTagParams,
|
||||
AshbyRemoveCandidateTagResponse
|
||||
> = {
|
||||
id: 'ashby_remove_candidate_tag',
|
||||
name: 'Ashby Remove Candidate Tag',
|
||||
description: 'Removes a tag from a candidate in Ashby.',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
apiKey: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Ashby API Key',
|
||||
},
|
||||
candidateId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'The UUID of the candidate to remove the tag from',
|
||||
},
|
||||
tagId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'The UUID of the tag to remove',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: 'https://api.ashbyhq.com/candidate.removeTag',
|
||||
method: 'POST',
|
||||
headers: (params) => ({
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Basic ${btoa(`${params.apiKey}:`)}`,
|
||||
}),
|
||||
body: (params) => ({
|
||||
candidateId: params.candidateId,
|
||||
tagId: params.tagId,
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
const data = await response.json()
|
||||
|
||||
if (!data.success) {
|
||||
throw new Error(data.errorInfo?.message || 'Failed to remove tag from candidate')
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
success: true,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
success: { type: 'boolean', description: 'Whether the tag was successfully removed' },
|
||||
},
|
||||
}
|
||||
@@ -80,6 +80,8 @@ export const searchCandidatesTool: ToolConfig<
|
||||
isPrimary: c.primaryPhoneNumber.isPrimary ?? true,
|
||||
}
|
||||
: null,
|
||||
createdAt: c.createdAt ?? null,
|
||||
updatedAt: c.updatedAt ?? null,
|
||||
})
|
||||
),
|
||||
},
|
||||
@@ -115,6 +117,8 @@ export const searchCandidatesTool: ToolConfig<
|
||||
isPrimary: { type: 'boolean', description: 'Whether this is the primary phone' },
|
||||
},
|
||||
},
|
||||
createdAt: { type: 'string', description: 'ISO 8601 creation timestamp' },
|
||||
updatedAt: { type: 'string', description: 'ISO 8601 last update timestamp' },
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -21,10 +21,8 @@ export interface AshbyGetCandidateParams extends AshbyBaseParams {
|
||||
|
||||
export interface AshbyCreateCandidateParams extends AshbyBaseParams {
|
||||
name: string
|
||||
email?: string
|
||||
emailType?: string
|
||||
email: string
|
||||
phoneNumber?: string
|
||||
phoneType?: string
|
||||
linkedInUrl?: string
|
||||
githubUrl?: string
|
||||
sourceId?: string
|
||||
@@ -111,6 +109,8 @@ export interface AshbySearchCandidatesResponse extends ToolResponse {
|
||||
name: string
|
||||
primaryEmailAddress: AshbyContactInfo | null
|
||||
primaryPhoneNumber: AshbyContactInfo | null
|
||||
createdAt: string
|
||||
updatedAt: string
|
||||
}>
|
||||
}
|
||||
}
|
||||
@@ -149,15 +149,7 @@ export interface AshbyGetJobResponse extends ToolResponse {
|
||||
|
||||
export interface AshbyCreateNoteResponse extends ToolResponse {
|
||||
output: {
|
||||
id: string
|
||||
content: string
|
||||
author: {
|
||||
id: string
|
||||
firstName: string
|
||||
lastName: string
|
||||
email: string
|
||||
} | null
|
||||
createdAt: string
|
||||
noteId: string
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -6,9 +6,7 @@ interface AshbyUpdateCandidateParams {
|
||||
candidateId: string
|
||||
name?: string
|
||||
email?: string
|
||||
emailType?: string
|
||||
phoneNumber?: string
|
||||
phoneType?: string
|
||||
linkedInUrl?: string
|
||||
githubUrl?: string
|
||||
websiteUrl?: string
|
||||
@@ -49,24 +47,12 @@ export const updateCandidateTool: ToolConfig<
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Updated primary email address',
|
||||
},
|
||||
emailType: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Email address type: Personal, Work, or Other (default Work)',
|
||||
},
|
||||
phoneNumber: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Updated primary phone number',
|
||||
},
|
||||
phoneType: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Phone number type: Personal, Work, or Other (default Work)',
|
||||
},
|
||||
linkedInUrl: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
@@ -105,27 +91,11 @@ export const updateCandidateTool: ToolConfig<
|
||||
candidateId: params.candidateId,
|
||||
}
|
||||
if (params.name) body.name = params.name
|
||||
if (params.email) {
|
||||
body.primaryEmailAddress = {
|
||||
value: params.email,
|
||||
type: params.emailType || 'Work',
|
||||
isPrimary: true,
|
||||
}
|
||||
}
|
||||
if (params.phoneNumber) {
|
||||
body.primaryPhoneNumber = {
|
||||
value: params.phoneNumber,
|
||||
type: params.phoneType || 'Work',
|
||||
isPrimary: true,
|
||||
}
|
||||
}
|
||||
if (params.linkedInUrl || params.githubUrl || params.websiteUrl) {
|
||||
const socialLinks: Array<{ url: string; type: string }> = []
|
||||
if (params.linkedInUrl) socialLinks.push({ url: params.linkedInUrl, type: 'LinkedIn' })
|
||||
if (params.githubUrl) socialLinks.push({ url: params.githubUrl, type: 'GitHub' })
|
||||
if (params.websiteUrl) socialLinks.push({ url: params.websiteUrl, type: 'Website' })
|
||||
body.socialLinks = socialLinks
|
||||
}
|
||||
if (params.email) body.email = params.email
|
||||
if (params.phoneNumber) body.phoneNumber = params.phoneNumber
|
||||
if (params.linkedInUrl) body.linkedInUrl = params.linkedInUrl
|
||||
if (params.githubUrl) body.githubUrl = params.githubUrl
|
||||
if (params.websiteUrl) body.websiteUrl = params.websiteUrl
|
||||
if (params.sourceId) body.sourceId = params.sourceId
|
||||
return body
|
||||
},
|
||||
|
||||
82
apps/sim/tools/box/copy_file.ts
Normal file
82
apps/sim/tools/box/copy_file.ts
Normal file
@@ -0,0 +1,82 @@
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
import type { BoxCopyFileParams, BoxUploadFileResponse } from './types'
|
||||
import { UPLOAD_FILE_OUTPUT_PROPERTIES } from './types'
|
||||
|
||||
export const boxCopyFileTool: ToolConfig<BoxCopyFileParams, BoxUploadFileResponse> = {
|
||||
id: 'box_copy_file',
|
||||
name: 'Box Copy File',
|
||||
description: 'Copy a file to another folder in Box',
|
||||
version: '1.0.0',
|
||||
|
||||
oauth: {
|
||||
required: true,
|
||||
provider: 'box',
|
||||
},
|
||||
|
||||
params: {
|
||||
accessToken: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'hidden',
|
||||
description: 'OAuth access token for Box API',
|
||||
},
|
||||
fileId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'The ID of the file to copy',
|
||||
},
|
||||
parentFolderId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'The ID of the destination folder',
|
||||
},
|
||||
name: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Optional new name for the copied file',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => `https://api.box.com/2.0/files/${params.fileId.trim()}/copy`,
|
||||
method: 'POST',
|
||||
headers: (params) => ({
|
||||
Authorization: `Bearer ${params.accessToken}`,
|
||||
'Content-Type': 'application/json',
|
||||
}),
|
||||
body: (params) => {
|
||||
const body: Record<string, unknown> = {
|
||||
parent: { id: params.parentFolderId.trim() },
|
||||
}
|
||||
if (params.name) body.name = params.name
|
||||
return body
|
||||
},
|
||||
},
|
||||
|
||||
transformResponse: async (response) => {
|
||||
const data = await response.json()
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(data.message || `Box API error: ${response.status}`)
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
id: data.id ?? '',
|
||||
name: data.name ?? '',
|
||||
size: data.size ?? 0,
|
||||
sha1: data.sha1 ?? null,
|
||||
createdAt: data.created_at ?? null,
|
||||
modifiedAt: data.modified_at ?? null,
|
||||
parentId: data.parent?.id ?? null,
|
||||
parentName: data.parent?.name ?? null,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: UPLOAD_FILE_OUTPUT_PROPERTIES,
|
||||
}
|
||||
71
apps/sim/tools/box/create_folder.ts
Normal file
71
apps/sim/tools/box/create_folder.ts
Normal file
@@ -0,0 +1,71 @@
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
import type { BoxCreateFolderParams, BoxFolderResponse } from './types'
|
||||
import { FOLDER_OUTPUT_PROPERTIES } from './types'
|
||||
|
||||
export const boxCreateFolderTool: ToolConfig<BoxCreateFolderParams, BoxFolderResponse> = {
|
||||
id: 'box_create_folder',
|
||||
name: 'Box Create Folder',
|
||||
description: 'Create a new folder in Box',
|
||||
version: '1.0.0',
|
||||
|
||||
oauth: {
|
||||
required: true,
|
||||
provider: 'box',
|
||||
},
|
||||
|
||||
params: {
|
||||
accessToken: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'hidden',
|
||||
description: 'OAuth access token for Box API',
|
||||
},
|
||||
name: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Name for the new folder',
|
||||
},
|
||||
parentFolderId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'The ID of the parent folder (use "0" for root)',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: 'https://api.box.com/2.0/folders',
|
||||
method: 'POST',
|
||||
headers: (params) => ({
|
||||
Authorization: `Bearer ${params.accessToken}`,
|
||||
'Content-Type': 'application/json',
|
||||
}),
|
||||
body: (params) => ({
|
||||
name: params.name,
|
||||
parent: { id: params.parentFolderId.trim() },
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response) => {
|
||||
const data = await response.json()
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(data.message || `Box API error: ${response.status}`)
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
id: data.id ?? '',
|
||||
name: data.name ?? '',
|
||||
createdAt: data.created_at ?? null,
|
||||
modifiedAt: data.modified_at ?? null,
|
||||
parentId: data.parent?.id ?? null,
|
||||
parentName: data.parent?.name ?? null,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: FOLDER_OUTPUT_PROPERTIES,
|
||||
}
|
||||
57
apps/sim/tools/box/delete_file.ts
Normal file
57
apps/sim/tools/box/delete_file.ts
Normal file
@@ -0,0 +1,57 @@
|
||||
import type { ToolConfig, ToolResponse } from '@/tools/types'
|
||||
import type { BoxDeleteFileParams } from './types'
|
||||
|
||||
export const boxDeleteFileTool: ToolConfig<BoxDeleteFileParams, ToolResponse> = {
|
||||
id: 'box_delete_file',
|
||||
name: 'Box Delete File',
|
||||
description: 'Delete a file from Box',
|
||||
version: '1.0.0',
|
||||
|
||||
oauth: {
|
||||
required: true,
|
||||
provider: 'box',
|
||||
},
|
||||
|
||||
params: {
|
||||
accessToken: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'hidden',
|
||||
description: 'OAuth access token for Box API',
|
||||
},
|
||||
fileId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'The ID of the file to delete',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => `https://api.box.com/2.0/files/${params.fileId.trim()}`,
|
||||
method: 'DELETE',
|
||||
headers: (params) => ({
|
||||
Authorization: `Bearer ${params.accessToken}`,
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response) => {
|
||||
if (response.status === 204) {
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
deleted: true,
|
||||
message: 'File deleted successfully',
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
throw new Error(data.message || `Box API error: ${response.status}`)
|
||||
},
|
||||
|
||||
outputs: {
|
||||
deleted: { type: 'boolean', description: 'Whether the file was successfully deleted' },
|
||||
message: { type: 'string', description: 'Success confirmation message' },
|
||||
},
|
||||
}
|
||||
68
apps/sim/tools/box/delete_folder.ts
Normal file
68
apps/sim/tools/box/delete_folder.ts
Normal file
@@ -0,0 +1,68 @@
|
||||
import type { ToolConfig, ToolResponse } from '@/tools/types'
|
||||
import type { BoxDeleteFolderParams } from './types'
|
||||
|
||||
export const boxDeleteFolderTool: ToolConfig<BoxDeleteFolderParams, ToolResponse> = {
|
||||
id: 'box_delete_folder',
|
||||
name: 'Box Delete Folder',
|
||||
description: 'Delete a folder from Box',
|
||||
version: '1.0.0',
|
||||
|
||||
oauth: {
|
||||
required: true,
|
||||
provider: 'box',
|
||||
},
|
||||
|
||||
params: {
|
||||
accessToken: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'hidden',
|
||||
description: 'OAuth access token for Box API',
|
||||
},
|
||||
folderId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'The ID of the folder to delete',
|
||||
},
|
||||
recursive: {
|
||||
type: 'boolean',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Delete folder and all its contents recursively',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => {
|
||||
const queryParams = new URLSearchParams()
|
||||
if (params.recursive) queryParams.set('recursive', 'true')
|
||||
const qs = queryParams.toString()
|
||||
return `https://api.box.com/2.0/folders/${params.folderId.trim()}${qs ? `?${qs}` : ''}`
|
||||
},
|
||||
method: 'DELETE',
|
||||
headers: (params) => ({
|
||||
Authorization: `Bearer ${params.accessToken}`,
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response) => {
|
||||
if (response.status === 204) {
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
deleted: true,
|
||||
message: 'Folder deleted successfully',
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
throw new Error(data.message || `Box API error: ${response.status}`)
|
||||
},
|
||||
|
||||
outputs: {
|
||||
deleted: { type: 'boolean', description: 'Whether the folder was successfully deleted' },
|
||||
message: { type: 'string', description: 'Success confirmation message' },
|
||||
},
|
||||
}
|
||||
87
apps/sim/tools/box/download_file.ts
Normal file
87
apps/sim/tools/box/download_file.ts
Normal file
@@ -0,0 +1,87 @@
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
import type { BoxDownloadFileParams, BoxDownloadFileResponse } from './types'
|
||||
|
||||
export const boxDownloadFileTool: ToolConfig<BoxDownloadFileParams, BoxDownloadFileResponse> = {
|
||||
id: 'box_download_file',
|
||||
name: 'Box Download File',
|
||||
description: 'Download a file from Box',
|
||||
version: '1.0.0',
|
||||
|
||||
oauth: {
|
||||
required: true,
|
||||
provider: 'box',
|
||||
},
|
||||
|
||||
params: {
|
||||
accessToken: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'hidden',
|
||||
description: 'OAuth access token for Box API',
|
||||
},
|
||||
fileId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'The ID of the file to download',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => `https://api.box.com/2.0/files/${params.fileId.trim()}/content`,
|
||||
method: 'GET',
|
||||
headers: (params) => ({
|
||||
Authorization: `Bearer ${params.accessToken}`,
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response) => {
|
||||
if (response.status === 202) {
|
||||
const retryAfter = response.headers.get('retry-after') || 'a few'
|
||||
throw new Error(`File is not yet ready for download. Retry after ${retryAfter} seconds.`)
|
||||
}
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text()
|
||||
throw new Error(errorText || `Failed to download file: ${response.status}`)
|
||||
}
|
||||
|
||||
const contentType = response.headers.get('content-type') || 'application/octet-stream'
|
||||
const contentDisposition = response.headers.get('content-disposition')
|
||||
let fileName = 'download'
|
||||
|
||||
if (contentDisposition) {
|
||||
const match = contentDisposition.match(/filename[^;=\n]*=((['"]).*?\2|[^;\n]*)/)
|
||||
if (match?.[1]) {
|
||||
fileName = match[1].replace(/['"]/g, '')
|
||||
}
|
||||
}
|
||||
|
||||
const arrayBuffer = await response.arrayBuffer()
|
||||
const buffer = Buffer.from(arrayBuffer)
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
file: {
|
||||
name: fileName,
|
||||
mimeType: contentType,
|
||||
data: buffer.toString('base64'),
|
||||
size: buffer.length,
|
||||
},
|
||||
content: buffer.toString('base64'),
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
file: {
|
||||
type: 'file',
|
||||
description: 'Downloaded file stored in execution files',
|
||||
},
|
||||
content: {
|
||||
type: 'string',
|
||||
description: 'Base64 encoded file content',
|
||||
},
|
||||
},
|
||||
}
|
||||
87
apps/sim/tools/box/get_file_info.ts
Normal file
87
apps/sim/tools/box/get_file_info.ts
Normal file
@@ -0,0 +1,87 @@
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
import type { BoxFileInfoResponse, BoxGetFileInfoParams } from './types'
|
||||
import { FILE_OUTPUT_PROPERTIES } from './types'
|
||||
|
||||
export const boxGetFileInfoTool: ToolConfig<BoxGetFileInfoParams, BoxFileInfoResponse> = {
|
||||
id: 'box_get_file_info',
|
||||
name: 'Box Get File Info',
|
||||
description: 'Get detailed information about a file in Box',
|
||||
version: '1.0.0',
|
||||
|
||||
oauth: {
|
||||
required: true,
|
||||
provider: 'box',
|
||||
},
|
||||
|
||||
params: {
|
||||
accessToken: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'hidden',
|
||||
description: 'OAuth access token for Box API',
|
||||
},
|
||||
fileId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'The ID of the file to get information about',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => `https://api.box.com/2.0/files/${params.fileId.trim()}`,
|
||||
method: 'GET',
|
||||
headers: (params) => ({
|
||||
Authorization: `Bearer ${params.accessToken}`,
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response) => {
|
||||
const data = await response.json()
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(data.message || `Box API error: ${response.status}`)
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
id: data.id ?? '',
|
||||
name: data.name ?? '',
|
||||
description: data.description ?? null,
|
||||
size: data.size ?? 0,
|
||||
sha1: data.sha1 ?? null,
|
||||
createdAt: data.created_at ?? null,
|
||||
modifiedAt: data.modified_at ?? null,
|
||||
createdBy: data.created_by
|
||||
? {
|
||||
id: data.created_by.id,
|
||||
name: data.created_by.name,
|
||||
login: data.created_by.login,
|
||||
}
|
||||
: null,
|
||||
modifiedBy: data.modified_by
|
||||
? {
|
||||
id: data.modified_by.id,
|
||||
name: data.modified_by.name,
|
||||
login: data.modified_by.login,
|
||||
}
|
||||
: null,
|
||||
ownedBy: data.owned_by
|
||||
? {
|
||||
id: data.owned_by.id,
|
||||
name: data.owned_by.name,
|
||||
login: data.owned_by.login,
|
||||
}
|
||||
: null,
|
||||
parentId: data.parent?.id ?? null,
|
||||
parentName: data.parent?.name ?? null,
|
||||
sharedLink: data.shared_link ?? null,
|
||||
tags: data.tags ?? [],
|
||||
commentCount: data.comment_count ?? null,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: FILE_OUTPUT_PROPERTIES,
|
||||
}
|
||||
10
apps/sim/tools/box/index.ts
Normal file
10
apps/sim/tools/box/index.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
export { boxCopyFileTool } from '@/tools/box/copy_file'
|
||||
export { boxCreateFolderTool } from '@/tools/box/create_folder'
|
||||
export { boxDeleteFileTool } from '@/tools/box/delete_file'
|
||||
export { boxDeleteFolderTool } from '@/tools/box/delete_folder'
|
||||
export { boxDownloadFileTool } from '@/tools/box/download_file'
|
||||
export { boxGetFileInfoTool } from '@/tools/box/get_file_info'
|
||||
export { boxListFolderItemsTool } from '@/tools/box/list_folder_items'
|
||||
export { boxSearchTool } from '@/tools/box/search'
|
||||
export { boxUpdateFileTool } from '@/tools/box/update_file'
|
||||
export { boxUploadFileTool } from '@/tools/box/upload_file'
|
||||
98
apps/sim/tools/box/list_folder_items.ts
Normal file
98
apps/sim/tools/box/list_folder_items.ts
Normal file
@@ -0,0 +1,98 @@
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
import type { BoxFolderItemsResponse, BoxListFolderItemsParams } from './types'
|
||||
import { FOLDER_ITEMS_OUTPUT_PROPERTIES } from './types'
|
||||
|
||||
export const boxListFolderItemsTool: ToolConfig<BoxListFolderItemsParams, BoxFolderItemsResponse> =
|
||||
{
|
||||
id: 'box_list_folder_items',
|
||||
name: 'Box List Folder Items',
|
||||
description: 'List files and folders in a Box folder',
|
||||
version: '1.0.0',
|
||||
|
||||
oauth: {
|
||||
required: true,
|
||||
provider: 'box',
|
||||
},
|
||||
|
||||
params: {
|
||||
accessToken: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'hidden',
|
||||
description: 'OAuth access token for Box API',
|
||||
},
|
||||
folderId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'The ID of the folder to list items from (use "0" for root)',
|
||||
},
|
||||
limit: {
|
||||
type: 'number',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Maximum number of items to return per page',
|
||||
},
|
||||
offset: {
|
||||
type: 'number',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'The offset for pagination',
|
||||
},
|
||||
sort: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Sort field: id, name, date, or size',
|
||||
},
|
||||
direction: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Sort direction: ASC or DESC',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => {
|
||||
const queryParams = new URLSearchParams()
|
||||
if (params.limit !== undefined) queryParams.set('limit', String(params.limit))
|
||||
if (params.offset !== undefined) queryParams.set('offset', String(params.offset))
|
||||
if (params.sort) queryParams.set('sort', params.sort)
|
||||
if (params.direction) queryParams.set('direction', params.direction)
|
||||
const qs = queryParams.toString()
|
||||
return `https://api.box.com/2.0/folders/${params.folderId.trim()}/items${qs ? `?${qs}` : ''}`
|
||||
},
|
||||
method: 'GET',
|
||||
headers: (params) => ({
|
||||
Authorization: `Bearer ${params.accessToken}`,
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response) => {
|
||||
const data = await response.json()
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(data.message || `Box API error: ${response.status}`)
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
entries: (data.entries ?? []).map((item: Record<string, unknown>) => ({
|
||||
type: item.type ?? '',
|
||||
id: item.id ?? '',
|
||||
name: item.name ?? '',
|
||||
size: item.size ?? null,
|
||||
createdAt: item.created_at ?? null,
|
||||
modifiedAt: item.modified_at ?? null,
|
||||
})),
|
||||
totalCount: data.total_count ?? 0,
|
||||
offset: data.offset ?? 0,
|
||||
limit: data.limit ?? 0,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: FOLDER_ITEMS_OUTPUT_PROPERTIES,
|
||||
}
|
||||
105
apps/sim/tools/box/search.ts
Normal file
105
apps/sim/tools/box/search.ts
Normal file
@@ -0,0 +1,105 @@
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
import type { BoxSearchParams, BoxSearchResponse } from './types'
|
||||
import { SEARCH_RESULT_OUTPUT_PROPERTIES } from './types'
|
||||
|
||||
export const boxSearchTool: ToolConfig<BoxSearchParams, BoxSearchResponse> = {
|
||||
id: 'box_search',
|
||||
name: 'Box Search',
|
||||
description: 'Search for files and folders in Box',
|
||||
version: '1.0.0',
|
||||
|
||||
oauth: {
|
||||
required: true,
|
||||
provider: 'box',
|
||||
},
|
||||
|
||||
params: {
|
||||
accessToken: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'hidden',
|
||||
description: 'OAuth access token for Box API',
|
||||
},
|
||||
query: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'The search query string',
|
||||
},
|
||||
limit: {
|
||||
type: 'number',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Maximum number of results to return',
|
||||
},
|
||||
offset: {
|
||||
type: 'number',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'The offset for pagination',
|
||||
},
|
||||
ancestorFolderId: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Restrict search to a specific folder and its subfolders',
|
||||
},
|
||||
fileExtensions: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Comma-separated file extensions to filter by (e.g., pdf,docx)',
|
||||
},
|
||||
type: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Restrict to a specific content type: file, folder, or web_link',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => {
|
||||
const queryParams = new URLSearchParams()
|
||||
queryParams.set('query', params.query)
|
||||
if (params.limit !== undefined) queryParams.set('limit', String(params.limit))
|
||||
if (params.offset !== undefined) queryParams.set('offset', String(params.offset))
|
||||
if (params.ancestorFolderId)
|
||||
queryParams.set('ancestor_folder_ids', params.ancestorFolderId.trim())
|
||||
if (params.fileExtensions) queryParams.set('file_extensions', params.fileExtensions)
|
||||
if (params.type) queryParams.set('type', params.type)
|
||||
return `https://api.box.com/2.0/search?${queryParams.toString()}`
|
||||
},
|
||||
method: 'GET',
|
||||
headers: (params) => ({
|
||||
Authorization: `Bearer ${params.accessToken}`,
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response) => {
|
||||
const data = await response.json()
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(data.message || `Box API error: ${response.status}`)
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
results: (data.entries ?? []).map((item: Record<string, unknown>) => ({
|
||||
type: item.type ?? '',
|
||||
id: item.id ?? '',
|
||||
name: item.name ?? '',
|
||||
size: item.size ?? null,
|
||||
createdAt: item.created_at ?? null,
|
||||
modifiedAt: item.modified_at ?? null,
|
||||
parentId: (item.parent as Record<string, unknown> | undefined)?.id ?? null,
|
||||
parentName: (item.parent as Record<string, unknown> | undefined)?.name ?? null,
|
||||
})),
|
||||
totalCount: data.total_count ?? 0,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: SEARCH_RESULT_OUTPUT_PROPERTIES,
|
||||
}
|
||||
249
apps/sim/tools/box/types.ts
Normal file
249
apps/sim/tools/box/types.ts
Normal file
@@ -0,0 +1,249 @@
|
||||
import type { OutputProperty, ToolResponse } from '@/tools/types'
|
||||
|
||||
export interface BoxUploadFileParams {
|
||||
accessToken: string
|
||||
parentFolderId: string
|
||||
file?: unknown
|
||||
fileContent?: string
|
||||
fileName?: string
|
||||
}
|
||||
|
||||
export interface BoxDownloadFileParams {
|
||||
accessToken: string
|
||||
fileId: string
|
||||
}
|
||||
|
||||
export interface BoxGetFileInfoParams {
|
||||
accessToken: string
|
||||
fileId: string
|
||||
}
|
||||
|
||||
export interface BoxListFolderItemsParams {
|
||||
accessToken: string
|
||||
folderId: string
|
||||
limit?: number
|
||||
offset?: number
|
||||
sort?: string
|
||||
direction?: string
|
||||
}
|
||||
|
||||
export interface BoxCreateFolderParams {
|
||||
accessToken: string
|
||||
name: string
|
||||
parentFolderId: string
|
||||
}
|
||||
|
||||
export interface BoxDeleteFileParams {
|
||||
accessToken: string
|
||||
fileId: string
|
||||
}
|
||||
|
||||
export interface BoxDeleteFolderParams {
|
||||
accessToken: string
|
||||
folderId: string
|
||||
recursive?: boolean
|
||||
}
|
||||
|
||||
export interface BoxCopyFileParams {
|
||||
accessToken: string
|
||||
fileId: string
|
||||
parentFolderId: string
|
||||
name?: string
|
||||
}
|
||||
|
||||
export interface BoxSearchParams {
|
||||
accessToken: string
|
||||
query: string
|
||||
limit?: number
|
||||
offset?: number
|
||||
ancestorFolderId?: string
|
||||
fileExtensions?: string
|
||||
type?: string
|
||||
}
|
||||
|
||||
export interface BoxUpdateFileParams {
|
||||
accessToken: string
|
||||
fileId: string
|
||||
name?: string
|
||||
description?: string
|
||||
parentFolderId?: string
|
||||
tags?: string
|
||||
}
|
||||
|
||||
export interface BoxUploadFileResponse extends ToolResponse {
|
||||
output: {
|
||||
id: string
|
||||
name: string
|
||||
size: number
|
||||
sha1: string | null
|
||||
createdAt: string | null
|
||||
modifiedAt: string | null
|
||||
parentId: string | null
|
||||
parentName: string | null
|
||||
}
|
||||
}
|
||||
|
||||
export interface BoxDownloadFileResponse extends ToolResponse {
|
||||
output: {
|
||||
file: {
|
||||
name: string
|
||||
mimeType: string
|
||||
data: string
|
||||
size: number
|
||||
}
|
||||
content: string
|
||||
}
|
||||
}
|
||||
|
||||
export interface BoxFileInfoResponse extends ToolResponse {
|
||||
output: {
|
||||
id: string
|
||||
name: string
|
||||
description: string | null
|
||||
size: number
|
||||
sha1: string | null
|
||||
createdAt: string | null
|
||||
modifiedAt: string | null
|
||||
createdBy: { id: string; name: string; login: string } | null
|
||||
modifiedBy: { id: string; name: string; login: string } | null
|
||||
ownedBy: { id: string; name: string; login: string } | null
|
||||
parentId: string | null
|
||||
parentName: string | null
|
||||
sharedLink: Record<string, unknown> | null
|
||||
tags: string[]
|
||||
commentCount: number | null
|
||||
}
|
||||
}
|
||||
|
||||
export interface BoxFolderItemsResponse extends ToolResponse {
|
||||
output: {
|
||||
entries: Array<Record<string, unknown>>
|
||||
totalCount: number
|
||||
offset: number
|
||||
limit: number
|
||||
}
|
||||
}
|
||||
|
||||
export interface BoxFolderResponse extends ToolResponse {
|
||||
output: {
|
||||
id: string
|
||||
name: string
|
||||
createdAt: string | null
|
||||
modifiedAt: string | null
|
||||
parentId: string | null
|
||||
parentName: string | null
|
||||
}
|
||||
}
|
||||
|
||||
export interface BoxSearchResponse extends ToolResponse {
|
||||
output: {
|
||||
results: Array<Record<string, unknown>>
|
||||
totalCount: number
|
||||
}
|
||||
}
|
||||
|
||||
const USER_OUTPUT_PROPERTIES = {
|
||||
id: { type: 'string', description: 'User ID' },
|
||||
name: { type: 'string', description: 'User name' },
|
||||
login: { type: 'string', description: 'User email/login' },
|
||||
} as const satisfies Record<string, OutputProperty>
|
||||
|
||||
export const FILE_OUTPUT_PROPERTIES = {
|
||||
id: { type: 'string', description: 'File ID' },
|
||||
name: { type: 'string', description: 'File name' },
|
||||
description: { type: 'string', description: 'File description', optional: true },
|
||||
size: { type: 'number', description: 'File size in bytes' },
|
||||
sha1: { type: 'string', description: 'SHA1 hash of file content', optional: true },
|
||||
createdAt: { type: 'string', description: 'Creation timestamp', optional: true },
|
||||
modifiedAt: { type: 'string', description: 'Last modified timestamp', optional: true },
|
||||
createdBy: {
|
||||
type: 'object',
|
||||
description: 'User who created the file',
|
||||
optional: true,
|
||||
properties: USER_OUTPUT_PROPERTIES,
|
||||
},
|
||||
modifiedBy: {
|
||||
type: 'object',
|
||||
description: 'User who last modified the file',
|
||||
optional: true,
|
||||
properties: USER_OUTPUT_PROPERTIES,
|
||||
},
|
||||
ownedBy: {
|
||||
type: 'object',
|
||||
description: 'User who owns the file',
|
||||
optional: true,
|
||||
properties: USER_OUTPUT_PROPERTIES,
|
||||
},
|
||||
parentId: { type: 'string', description: 'Parent folder ID', optional: true },
|
||||
parentName: { type: 'string', description: 'Parent folder name', optional: true },
|
||||
sharedLink: { type: 'json', description: 'Shared link details', optional: true },
|
||||
tags: {
|
||||
type: 'array',
|
||||
description: 'File tags',
|
||||
items: { type: 'string' },
|
||||
optional: true,
|
||||
},
|
||||
commentCount: { type: 'number', description: 'Number of comments', optional: true },
|
||||
} as const satisfies Record<string, OutputProperty>
|
||||
|
||||
export const FOLDER_ITEMS_OUTPUT_PROPERTIES = {
|
||||
entries: {
|
||||
type: 'array',
|
||||
description: 'List of items in the folder',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
type: { type: 'string', description: 'Item type (file, folder, web_link)' },
|
||||
id: { type: 'string', description: 'Item ID' },
|
||||
name: { type: 'string', description: 'Item name' },
|
||||
size: { type: 'number', description: 'Item size in bytes', optional: true },
|
||||
createdAt: { type: 'string', description: 'Creation timestamp', optional: true },
|
||||
modifiedAt: { type: 'string', description: 'Last modified timestamp', optional: true },
|
||||
},
|
||||
},
|
||||
},
|
||||
totalCount: { type: 'number', description: 'Total number of items in the folder' },
|
||||
offset: { type: 'number', description: 'Current pagination offset' },
|
||||
limit: { type: 'number', description: 'Current pagination limit' },
|
||||
} as const satisfies Record<string, OutputProperty>
|
||||
|
||||
export const FOLDER_OUTPUT_PROPERTIES = {
|
||||
id: { type: 'string', description: 'Folder ID' },
|
||||
name: { type: 'string', description: 'Folder name' },
|
||||
createdAt: { type: 'string', description: 'Creation timestamp', optional: true },
|
||||
modifiedAt: { type: 'string', description: 'Last modified timestamp', optional: true },
|
||||
parentId: { type: 'string', description: 'Parent folder ID', optional: true },
|
||||
parentName: { type: 'string', description: 'Parent folder name', optional: true },
|
||||
} as const satisfies Record<string, OutputProperty>
|
||||
|
||||
export const SEARCH_RESULT_OUTPUT_PROPERTIES = {
|
||||
results: {
|
||||
type: 'array',
|
||||
description: 'Search results',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
type: { type: 'string', description: 'Item type (file, folder, web_link)' },
|
||||
id: { type: 'string', description: 'Item ID' },
|
||||
name: { type: 'string', description: 'Item name' },
|
||||
size: { type: 'number', description: 'Item size in bytes', optional: true },
|
||||
createdAt: { type: 'string', description: 'Creation timestamp', optional: true },
|
||||
modifiedAt: { type: 'string', description: 'Last modified timestamp', optional: true },
|
||||
parentId: { type: 'string', description: 'Parent folder ID', optional: true },
|
||||
parentName: { type: 'string', description: 'Parent folder name', optional: true },
|
||||
},
|
||||
},
|
||||
},
|
||||
totalCount: { type: 'number', description: 'Total number of matching results' },
|
||||
} as const satisfies Record<string, OutputProperty>
|
||||
|
||||
export const UPLOAD_FILE_OUTPUT_PROPERTIES = {
|
||||
id: { type: 'string', description: 'File ID' },
|
||||
name: { type: 'string', description: 'File name' },
|
||||
size: { type: 'number', description: 'File size in bytes' },
|
||||
sha1: { type: 'string', description: 'SHA1 hash of file content', optional: true },
|
||||
createdAt: { type: 'string', description: 'Creation timestamp', optional: true },
|
||||
modifiedAt: { type: 'string', description: 'Last modified timestamp', optional: true },
|
||||
parentId: { type: 'string', description: 'Parent folder ID', optional: true },
|
||||
parentName: { type: 'string', description: 'Parent folder name', optional: true },
|
||||
} as const satisfies Record<string, OutputProperty>
|
||||
124
apps/sim/tools/box/update_file.ts
Normal file
124
apps/sim/tools/box/update_file.ts
Normal file
@@ -0,0 +1,124 @@
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
import type { BoxFileInfoResponse, BoxUpdateFileParams } from './types'
|
||||
import { FILE_OUTPUT_PROPERTIES } from './types'
|
||||
|
||||
export const boxUpdateFileTool: ToolConfig<BoxUpdateFileParams, BoxFileInfoResponse> = {
|
||||
id: 'box_update_file',
|
||||
name: 'Box Update File',
|
||||
description: 'Update file info in Box (rename, move, change description, add tags)',
|
||||
version: '1.0.0',
|
||||
|
||||
oauth: {
|
||||
required: true,
|
||||
provider: 'box',
|
||||
},
|
||||
|
||||
params: {
|
||||
accessToken: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'hidden',
|
||||
description: 'OAuth access token for Box API',
|
||||
},
|
||||
fileId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'The ID of the file to update',
|
||||
},
|
||||
name: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'New name for the file',
|
||||
},
|
||||
description: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'New description for the file (max 256 characters)',
|
||||
},
|
||||
parentFolderId: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Move the file to a different folder by specifying the folder ID',
|
||||
},
|
||||
tags: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Comma-separated tags to set on the file',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => `https://api.box.com/2.0/files/${params.fileId.trim()}`,
|
||||
method: 'PUT',
|
||||
headers: (params) => ({
|
||||
Authorization: `Bearer ${params.accessToken}`,
|
||||
'Content-Type': 'application/json',
|
||||
}),
|
||||
body: (params) => {
|
||||
const body: Record<string, unknown> = {}
|
||||
if (params.name) body.name = params.name
|
||||
if (params.description !== undefined) body.description = params.description
|
||||
if (params.parentFolderId) body.parent = { id: params.parentFolderId.trim() }
|
||||
if (params.tags)
|
||||
body.tags = params.tags
|
||||
.split(',')
|
||||
.map((t: string) => t.trim())
|
||||
.filter(Boolean)
|
||||
return body
|
||||
},
|
||||
},
|
||||
|
||||
transformResponse: async (response) => {
|
||||
const data = await response.json()
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(data.message || `Box API error: ${response.status}`)
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
id: data.id ?? '',
|
||||
name: data.name ?? '',
|
||||
description: data.description ?? null,
|
||||
size: data.size ?? 0,
|
||||
sha1: data.sha1 ?? null,
|
||||
createdAt: data.created_at ?? null,
|
||||
modifiedAt: data.modified_at ?? null,
|
||||
createdBy: data.created_by
|
||||
? {
|
||||
id: data.created_by.id,
|
||||
name: data.created_by.name,
|
||||
login: data.created_by.login,
|
||||
}
|
||||
: null,
|
||||
modifiedBy: data.modified_by
|
||||
? {
|
||||
id: data.modified_by.id,
|
||||
name: data.modified_by.name,
|
||||
login: data.modified_by.login,
|
||||
}
|
||||
: null,
|
||||
ownedBy: data.owned_by
|
||||
? {
|
||||
id: data.owned_by.id,
|
||||
name: data.owned_by.name,
|
||||
login: data.owned_by.login,
|
||||
}
|
||||
: null,
|
||||
parentId: data.parent?.id ?? null,
|
||||
parentName: data.parent?.name ?? null,
|
||||
sharedLink: data.shared_link ?? null,
|
||||
tags: data.tags ?? [],
|
||||
commentCount: data.comment_count ?? null,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: FILE_OUTPUT_PROPERTIES,
|
||||
}
|
||||
78
apps/sim/tools/box/upload_file.ts
Normal file
78
apps/sim/tools/box/upload_file.ts
Normal file
@@ -0,0 +1,78 @@
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
import type { BoxUploadFileParams, BoxUploadFileResponse } from './types'
|
||||
import { UPLOAD_FILE_OUTPUT_PROPERTIES } from './types'
|
||||
|
||||
export const boxUploadFileTool: ToolConfig<BoxUploadFileParams, BoxUploadFileResponse> = {
|
||||
id: 'box_upload_file',
|
||||
name: 'Box Upload File',
|
||||
description: 'Upload a file to a Box folder',
|
||||
version: '1.0.0',
|
||||
|
||||
oauth: {
|
||||
required: true,
|
||||
provider: 'box',
|
||||
},
|
||||
|
||||
params: {
|
||||
accessToken: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'hidden',
|
||||
description: 'OAuth access token for Box API',
|
||||
},
|
||||
parentFolderId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'The ID of the folder to upload the file to (use "0" for root)',
|
||||
},
|
||||
file: {
|
||||
type: 'file',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'The file to upload (UserFile object)',
|
||||
},
|
||||
fileContent: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'hidden',
|
||||
description: 'Legacy: base64 encoded file content',
|
||||
},
|
||||
fileName: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Optional filename override',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: '/api/tools/box/upload',
|
||||
method: 'POST',
|
||||
headers: () => ({
|
||||
'Content-Type': 'application/json',
|
||||
}),
|
||||
body: (params) => ({
|
||||
accessToken: params.accessToken,
|
||||
parentFolderId: params.parentFolderId,
|
||||
file: params.file,
|
||||
fileContent: params.fileContent,
|
||||
fileName: params.fileName,
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response) => {
|
||||
const data = await response.json()
|
||||
|
||||
if (!data.success) {
|
||||
throw new Error(data.error || 'Failed to upload file')
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: data.output,
|
||||
}
|
||||
},
|
||||
|
||||
outputs: UPLOAD_FILE_OUTPUT_PROPERTIES,
|
||||
}
|
||||
80
apps/sim/tools/box_sign/cancel_request.ts
Normal file
80
apps/sim/tools/box_sign/cancel_request.ts
Normal file
@@ -0,0 +1,80 @@
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
import type { BoxSignCancelRequestParams, BoxSignResponse } from './types'
|
||||
import { SIGN_REQUEST_OUTPUT_PROPERTIES } from './types'
|
||||
|
||||
export const boxSignCancelRequestTool: ToolConfig<BoxSignCancelRequestParams, BoxSignResponse> = {
|
||||
id: 'box_sign_cancel_request',
|
||||
name: 'Box Sign Cancel Request',
|
||||
description: 'Cancel a pending Box Sign request',
|
||||
version: '1.0.0',
|
||||
|
||||
oauth: {
|
||||
required: true,
|
||||
provider: 'box',
|
||||
},
|
||||
|
||||
params: {
|
||||
accessToken: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'hidden',
|
||||
description: 'OAuth access token for Box API',
|
||||
},
|
||||
signRequestId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'The ID of the sign request to cancel',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => `https://api.box.com/2.0/sign_requests/${params.signRequestId}/cancel`,
|
||||
method: 'POST',
|
||||
headers: (params) => ({
|
||||
Authorization: `Bearer ${params.accessToken}`,
|
||||
'Content-Type': 'application/json',
|
||||
}),
|
||||
body: () => ({}),
|
||||
},
|
||||
|
||||
transformResponse: async (response) => {
|
||||
const data = await response.json()
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(data.message || `Box Sign API error: ${response.status}`)
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
id: data.id ?? '',
|
||||
status: data.status ?? '',
|
||||
name: data.name ?? null,
|
||||
shortId: data.short_id ?? null,
|
||||
signers: (data.signers ?? []).map((s: Record<string, unknown>) => ({
|
||||
email: s.email ?? null,
|
||||
role: s.role ?? null,
|
||||
hasViewedDocument: s.has_viewed_document ?? null,
|
||||
signerDecision: s.signer_decision ?? null,
|
||||
embedUrl: s.embed_url ?? null,
|
||||
order: s.order ?? null,
|
||||
})),
|
||||
sourceFiles: (data.source_files ?? []).map((f: Record<string, unknown>) => ({
|
||||
id: f.id ?? null,
|
||||
type: f.type ?? null,
|
||||
name: f.name ?? null,
|
||||
})),
|
||||
emailSubject: data.email_subject ?? null,
|
||||
emailMessage: data.email_message ?? null,
|
||||
daysValid: data.days_valid ?? null,
|
||||
createdAt: data.created_at ?? null,
|
||||
autoExpireAt: data.auto_expire_at ?? null,
|
||||
prepareUrl: data.prepare_url ?? null,
|
||||
senderEmail: data.sender_email ?? null,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: SIGN_REQUEST_OUTPUT_PROPERTIES,
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user