mirror of
https://github.com/simstudioai/sim.git
synced 2026-03-15 03:00:33 -04:00
Compare commits
10 Commits
feat/tools
...
waleedlati
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c024526d74 | ||
|
|
774771fddd | ||
|
|
43c0f5b199 | ||
|
|
ff01825b20 | ||
|
|
58d0fda173 | ||
|
|
ecdb133d1b | ||
|
|
d06459f489 | ||
|
|
0574427d45 | ||
|
|
8f9b859a53 | ||
|
|
60f9eb21bf |
@@ -3552,6 +3552,15 @@ export function TrelloIcon(props: SVGProps<SVGSVGElement>) {
|
||||
)
|
||||
}
|
||||
|
||||
export function AttioIcon(props: SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
<svg {...props} xmlns='http://www.w3.org/2000/svg' viewBox='0 0 60.9 50' fill='currentColor'>
|
||||
<path d='M60.3,34.8l-5.1-8.1c0,0,0,0,0,0L54.7,26c-0.8-1.2-2.1-1.9-3.5-1.9L43,24L42.5,25l-9.8,15.7l-0.5,0.9l4.1,6.6c0.8,1.2,2.1,1.9,3.5,1.9h11.5c1.4,0,2.8-0.7,3.5-1.9l0.4-0.6c0,0,0,0,0,0l5.1-8.2C61.1,37.9,61.1,36.2,60.3,34.8L60.3,34.8z M58.7,38.3l-5.1,8.2c0,0,0,0.1-0.1,0.1c-0.2,0.2-0.4,0.2-0.5,0.2c-0.1,0-0.4,0-0.6-0.3l-5.1-8.2c-0.1-0.1-0.1-0.2-0.2-0.3c0-0.1-0.1-0.2-0.1-0.3c-0.1-0.4-0.1-0.8,0-1.3c0.1-0.2,0.1-0.4,0.3-0.6l5.1-8.1c0,0,0,0,0,0c0.1-0.2,0.3-0.3,0.4-0.3c0.1,0,0.1,0,0.1,0c0,0,0,0,0.1,0c0.1,0,0.4,0,0.6,0.3l5.1,8.1C59.2,36.6,59.2,37.5,58.7,38.3L58.7,38.3z' />
|
||||
<path d='M45.2,15.1c0.8-1.3,0.8-3.1,0-4.4l-5.1-8.1l-0.4-0.7C38.9,0.7,37.6,0,36.2,0H24.7c-1.4,0-2.7,0.7-3.5,1.9L0.6,34.9C0.2,35.5,0,36.3,0,37c0,0.8,0.2,1.5,0.6,2.2l5.5,8.8C6.9,49.3,8.2,50,9.7,50h11.5c1.4,0,2.8-0.7,3.5-1.9l0.4-0.7c0,0,0,0,0,0c0,0,0,0,0,0l4.1-6.6l12.1-19.4L45.2,15.1L45.2,15.1z M44,13c0,0.4-0.1,0.8-0.4,1.2L23.5,46.4c-0.2,0.3-0.5,0.3-0.6,0.3c-0.1,0-0.4,0-0.6-0.3l-5.1-8.2c-0.5-0.7-0.5-1.7,0-2.4L37.4,3.6c0.2-0.3,0.5-0.3,0.6-0.3c0.1,0,0.4,0,0.6,0.3l5.1,8.1C43.9,12.1,44,12.5,44,13z' />
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
export function AsanaIcon(props: SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
<svg {...props} xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none'>
|
||||
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
ApolloIcon,
|
||||
ArxivIcon,
|
||||
AsanaIcon,
|
||||
AttioIcon,
|
||||
BrainIcon,
|
||||
BrowserUseIcon,
|
||||
CalComIcon,
|
||||
@@ -37,8 +38,8 @@ import {
|
||||
EyeIcon,
|
||||
FirecrawlIcon,
|
||||
FirefliesIcon,
|
||||
GithubIcon,
|
||||
GitLabIcon,
|
||||
GithubIcon,
|
||||
GmailIcon,
|
||||
GongIcon,
|
||||
GoogleBooksIcon,
|
||||
@@ -71,9 +72,9 @@ import {
|
||||
LinearIcon,
|
||||
LinkedInIcon,
|
||||
LinkupIcon,
|
||||
MailServerIcon,
|
||||
MailchimpIcon,
|
||||
MailgunIcon,
|
||||
MailServerIcon,
|
||||
Mem0Icon,
|
||||
MicrosoftDataverseIcon,
|
||||
MicrosoftExcelIcon,
|
||||
@@ -106,6 +107,8 @@ import {
|
||||
ResendIcon,
|
||||
RevenueCatIcon,
|
||||
S3Icon,
|
||||
SQSIcon,
|
||||
STTIcon,
|
||||
SalesforceIcon,
|
||||
SearchIcon,
|
||||
SendgridIcon,
|
||||
@@ -117,19 +120,17 @@ import {
|
||||
SimilarwebIcon,
|
||||
SlackIcon,
|
||||
SmtpIcon,
|
||||
SQSIcon,
|
||||
SshIcon,
|
||||
STTIcon,
|
||||
StagehandIcon,
|
||||
StripeIcon,
|
||||
SupabaseIcon,
|
||||
TTSIcon,
|
||||
TavilyIcon,
|
||||
TelegramIcon,
|
||||
TextractIcon,
|
||||
TinybirdIcon,
|
||||
TranslateIcon,
|
||||
TrelloIcon,
|
||||
TTSIcon,
|
||||
TwilioIcon,
|
||||
TypeformIcon,
|
||||
UpstashIcon,
|
||||
@@ -140,11 +141,11 @@ import {
|
||||
WhatsAppIcon,
|
||||
WikipediaIcon,
|
||||
WordpressIcon,
|
||||
xIcon,
|
||||
YouTubeIcon,
|
||||
ZendeskIcon,
|
||||
ZepIcon,
|
||||
ZoomIcon,
|
||||
xIcon,
|
||||
} from '@/components/icons'
|
||||
|
||||
type IconComponent = ComponentType<SVGProps<SVGSVGElement>>
|
||||
@@ -159,6 +160,7 @@ export const blockTypeToIconMap: Record<string, IconComponent> = {
|
||||
apollo: ApolloIcon,
|
||||
arxiv: ArxivIcon,
|
||||
asana: AsanaIcon,
|
||||
attio: AttioIcon,
|
||||
browser_use: BrowserUseIcon,
|
||||
calcom: CalComIcon,
|
||||
calendly: CalendlyIcon,
|
||||
|
||||
@@ -1,96 +0,0 @@
|
||||
---
|
||||
title: Umgebungsvariablen
|
||||
---
|
||||
|
||||
import { Callout } from 'fumadocs-ui/components/callout'
|
||||
import { Image } from '@/components/ui/image'
|
||||
|
||||
Umgebungsvariablen bieten eine sichere Möglichkeit, Konfigurationswerte und Geheimnisse in Ihren Workflows zu verwalten, einschließlich API-Schlüssel und anderer sensibler Daten, auf die Ihre Workflows zugreifen müssen. Sie halten Geheimnisse aus Ihren Workflow-Definitionen heraus und machen sie während der Ausführung verfügbar.
|
||||
|
||||
## Variablentypen
|
||||
|
||||
Umgebungsvariablen in Sim funktionieren auf zwei Ebenen:
|
||||
|
||||
- **Persönliche Umgebungsvariablen**: Privat für Ihr Konto, nur Sie können sie sehen und verwenden
|
||||
- **Workspace-Umgebungsvariablen**: Werden im gesamten Workspace geteilt und sind für alle Teammitglieder verfügbar
|
||||
|
||||
<Callout type="info">
|
||||
Workspace-Umgebungsvariablen haben Vorrang vor persönlichen Variablen, wenn es einen Namenskonflikt gibt.
|
||||
</Callout>
|
||||
|
||||
## Einrichten von Umgebungsvariablen
|
||||
|
||||
Navigieren Sie zu den Einstellungen, um Ihre Umgebungsvariablen zu konfigurieren:
|
||||
|
||||
<Image
|
||||
src="/static/environment/environment-1.png"
|
||||
alt="Umgebungsvariablen-Modal zum Erstellen neuer Variablen"
|
||||
width={500}
|
||||
height={350}
|
||||
/>
|
||||
|
||||
In Ihren Workspace-Einstellungen können Sie sowohl persönliche als auch Workspace-Umgebungsvariablen erstellen und verwalten. Persönliche Variablen sind privat für Ihr Konto, während Workspace-Variablen mit allen Teammitgliedern geteilt werden.
|
||||
|
||||
### Variablen auf Workspace-Ebene setzen
|
||||
|
||||
Verwenden Sie den Workspace-Bereichsschalter, um Variablen für Ihr gesamtes Team verfügbar zu machen:
|
||||
|
||||
<Image
|
||||
src="/static/environment/environment-2.png"
|
||||
alt="Workspace-Bereich für Umgebungsvariablen umschalten"
|
||||
width={500}
|
||||
height={350}
|
||||
/>
|
||||
|
||||
Wenn Sie den Workspace-Bereich aktivieren, wird die Variable für alle Workspace-Mitglieder verfügbar und kann in jedem Workflow innerhalb dieses Workspaces verwendet werden.
|
||||
|
||||
### Ansicht der Workspace-Variablen
|
||||
|
||||
Sobald Sie Workspace-Variablen haben, erscheinen sie in Ihrer Liste der Umgebungsvariablen:
|
||||
|
||||
<Image
|
||||
src="/static/environment/environment-3.png"
|
||||
alt="Workspace-Variablen in der Liste der Umgebungsvariablen"
|
||||
width={500}
|
||||
height={350}
|
||||
/>
|
||||
|
||||
## Verwendung von Variablen in Workflows
|
||||
|
||||
Um Umgebungsvariablen in Ihren Workflows zu referenzieren, verwenden Sie die `{{}}` Notation. Wenn Sie `{{` in ein beliebiges Eingabefeld eingeben, erscheint ein Dropdown-Menü mit Ihren persönlichen und Workspace-Umgebungsvariablen. Wählen Sie einfach die Variable aus, die Sie verwenden möchten.
|
||||
|
||||
<Image
|
||||
src="/static/environment/environment-4.png"
|
||||
alt="Verwendung von Umgebungsvariablen mit doppelter Klammernotation"
|
||||
width={500}
|
||||
height={350}
|
||||
/>
|
||||
|
||||
## Wie Variablen aufgelöst werden
|
||||
|
||||
**Workspace-Variablen haben immer Vorrang** vor persönlichen Variablen, unabhängig davon, wer den Workflow ausführt.
|
||||
|
||||
Wenn keine Workspace-Variable für einen Schlüssel existiert, werden persönliche Variablen verwendet:
|
||||
- **Manuelle Ausführungen (UI)**: Ihre persönlichen Variablen
|
||||
- **Automatisierte Ausführungen (API, Webhook, Zeitplan, bereitgestellter Chat)**: Persönliche Variablen des Workflow-Besitzers
|
||||
|
||||
<Callout type="info">
|
||||
Persönliche Variablen eignen sich am besten zum Testen. Verwenden Sie Workspace-Variablen für Produktions-Workflows.
|
||||
</Callout>
|
||||
|
||||
## Sicherheits-Best-Practices
|
||||
|
||||
### Für sensible Daten
|
||||
- Speichern Sie API-Schlüssel, Tokens und Passwörter als Umgebungsvariablen anstatt sie im Code festzuschreiben
|
||||
- Verwenden Sie Workspace-Variablen für gemeinsam genutzte Ressourcen, die mehrere Teammitglieder benötigen
|
||||
- Bewahren Sie persönliche Anmeldedaten in persönlichen Variablen auf
|
||||
|
||||
### Variablenbenennung
|
||||
- Verwenden Sie beschreibende Namen: `DATABASE_URL` anstatt `DB`
|
||||
- Folgen Sie einheitlichen Benennungskonventionen in Ihrem Team
|
||||
- Erwägen Sie Präfixe, um Konflikte zu vermeiden: `PROD_API_KEY`, `DEV_API_KEY`
|
||||
|
||||
### Zugriffskontrolle
|
||||
- Workspace-Umgebungsvariablen respektieren Workspace-Berechtigungen
|
||||
- Nur Benutzer mit Schreibzugriff oder höher können Workspace-Variablen erstellen/ändern
|
||||
- Persönliche Variablen sind immer privat für den einzelnen Benutzer
|
||||
@@ -95,11 +95,17 @@ const apiUrl = `https://api.example.com/users/${userId}/profile`;
|
||||
|
||||
### Request Retries
|
||||
|
||||
The API block automatically handles:
|
||||
- Network timeouts with exponential backoff
|
||||
- Rate limit responses (429 status codes)
|
||||
- Server errors (5xx status codes) with retry logic
|
||||
- Connection failures with reconnection attempts
|
||||
The API block supports **configurable retries** (see the block’s **Advanced** settings):
|
||||
|
||||
- **Retries**: Number of retry attempts (additional tries after the first request)
|
||||
- **Retry delay (ms)**: Initial delay before retrying (uses exponential backoff)
|
||||
- **Max retry delay (ms)**: Maximum delay between retries
|
||||
- **Retry non-idempotent methods**: Allow retries for **POST/PATCH** (may create duplicate requests)
|
||||
|
||||
Retries are attempted for:
|
||||
|
||||
- Network/connection failures and timeouts (with exponential backoff)
|
||||
- Rate limits (**429**) and server errors (**5xx**)
|
||||
|
||||
### Response Validation
|
||||
|
||||
|
||||
192
apps/docs/content/docs/en/credentials/index.mdx
Normal file
192
apps/docs/content/docs/en/credentials/index.mdx
Normal file
@@ -0,0 +1,192 @@
|
||||
---
|
||||
title: Credentials
|
||||
description: Manage secrets, API keys, and OAuth connections for your workflows
|
||||
---
|
||||
|
||||
import { Callout } from 'fumadocs-ui/components/callout'
|
||||
import { Image } from '@/components/ui/image'
|
||||
import { Step, Steps } from 'fumadocs-ui/components/steps'
|
||||
|
||||
Credentials provide a secure way to manage API keys, tokens, and third-party service connections across your workflows. Instead of hardcoding sensitive values into your workflow, you store them as credentials and reference them at runtime.
|
||||
|
||||
Sim supports two categories of credentials: **secrets** for static values like API keys, and **OAuth accounts** for authenticated service connections like Google or Slack.
|
||||
|
||||
## Getting Started
|
||||
|
||||
To manage credentials, open your workspace **Settings** and navigate to the **Secrets** tab.
|
||||
|
||||
<Image
|
||||
src="/static/credentials/settings-secrets.png"
|
||||
alt="Settings modal showing the Secrets tab with a list of saved credentials"
|
||||
width={700}
|
||||
height={200}
|
||||
/>
|
||||
|
||||
From here you can search, create, and delete both secrets and OAuth connections.
|
||||
|
||||
## Secrets
|
||||
|
||||
Secrets are key-value pairs that store sensitive data like API keys, tokens, and passwords. Each secret has a **key** (used to reference it in workflows) and a **value** (the actual secret).
|
||||
|
||||
### Creating a Secret
|
||||
|
||||
<Image
|
||||
src="/static/credentials/create-secret.png"
|
||||
alt="Create Secret dialog with fields for key, value, description, and scope toggle"
|
||||
width={500}
|
||||
height={400}
|
||||
/>
|
||||
|
||||
<Steps>
|
||||
<Step>
|
||||
Click **+ Add** and select **Secret** as the type
|
||||
</Step>
|
||||
<Step>
|
||||
Enter a **Key** name (letters, numbers, and underscores only, e.g. `OPENAI_API_KEY`)
|
||||
</Step>
|
||||
<Step>
|
||||
Enter the **Value**
|
||||
</Step>
|
||||
<Step>
|
||||
Optionally add a **Description** to help your team understand what the secret is for
|
||||
</Step>
|
||||
<Step>
|
||||
Choose the **Scope** — Workspace or Personal
|
||||
</Step>
|
||||
<Step>
|
||||
Click **Create**
|
||||
</Step>
|
||||
</Steps>
|
||||
|
||||
### Using Secrets in Workflows
|
||||
|
||||
To reference a secret in any input field, type `{{` to open the dropdown. It will show your available secrets grouped by scope.
|
||||
|
||||
<Image
|
||||
src="/static/credentials/secret-dropdown.png"
|
||||
alt="Typing {{ in a code block opens a dropdown showing available workspace secrets"
|
||||
width={400}
|
||||
height={250}
|
||||
/>
|
||||
|
||||
Select the secret you want to use. The reference will appear highlighted in blue, indicating it will be resolved at runtime.
|
||||
|
||||
<Image
|
||||
src="/static/credentials/secret-resolved.png"
|
||||
alt="A resolved secret reference shown in blue text as {{OPENAI_API_KEY}}"
|
||||
width={400}
|
||||
height={200}
|
||||
/>
|
||||
|
||||
<Callout type="warn">
|
||||
Secret values are never exposed in the workflow editor or logs. They are only resolved during execution.
|
||||
</Callout>
|
||||
|
||||
### Bulk Import
|
||||
|
||||
You can import multiple secrets at once by pasting `.env`-style content:
|
||||
|
||||
1. Click **+ Add**, then switch to **Bulk** mode
|
||||
2. Paste your environment variables in `KEY=VALUE` format
|
||||
3. Choose the scope for all imported secrets
|
||||
4. Click **Create**
|
||||
|
||||
The parser supports standard `KEY=VALUE` pairs, quoted values, comments (`#`), and blank lines.
|
||||
|
||||
## OAuth Accounts
|
||||
|
||||
OAuth accounts are authenticated connections to third-party services like Google, Slack, GitHub, and more. Sim handles the OAuth flow, token storage, and automatic refresh.
|
||||
|
||||
You can connect **multiple accounts per provider** — for example, two separate Gmail accounts for different workflows.
|
||||
|
||||
### Connecting an OAuth Account
|
||||
|
||||
<Image
|
||||
src="/static/credentials/create-oauth.png"
|
||||
alt="Create Secret dialog with OAuth Account type selected, showing display name and provider dropdown"
|
||||
width={500}
|
||||
height={400}
|
||||
/>
|
||||
|
||||
<Steps>
|
||||
<Step>
|
||||
Click **+ Add** and select **OAuth Account** as the type
|
||||
</Step>
|
||||
<Step>
|
||||
Enter a **Display name** to identify this connection (e.g. "Work Gmail" or "Marketing Slack")
|
||||
</Step>
|
||||
<Step>
|
||||
Optionally add a **Description**
|
||||
</Step>
|
||||
<Step>
|
||||
Select the **Account** provider from the dropdown
|
||||
</Step>
|
||||
<Step>
|
||||
Click **Connect** and complete the authorization flow
|
||||
</Step>
|
||||
</Steps>
|
||||
|
||||
### Using OAuth Accounts in Workflows
|
||||
|
||||
Blocks that require authentication (e.g. Gmail, Slack, Google Sheets) display a credential selector dropdown. Select the OAuth account you want the block to use.
|
||||
|
||||
<Image
|
||||
src="/static/credentials/oauth-selector.png"
|
||||
alt="Gmail block showing the account selector dropdown with a connected account and option to connect another"
|
||||
width={500}
|
||||
height={350}
|
||||
/>
|
||||
|
||||
You can also connect additional accounts directly from the block by selecting **Connect another account** at the bottom of the dropdown.
|
||||
|
||||
<Callout type="info">
|
||||
If a block requires an OAuth connection and none is selected, the workflow will fail at that step.
|
||||
</Callout>
|
||||
|
||||
## Workspace vs. Personal
|
||||
|
||||
Credentials can be scoped to your **workspace** (shared with your team) or kept **personal** (private to you).
|
||||
|
||||
| | Workspace | Personal |
|
||||
|---|---|---|
|
||||
| **Visibility** | All workspace members | Only you |
|
||||
| **Use in workflows** | Any member can use | Only you can use |
|
||||
| **Best for** | Production workflows, shared services | Testing, personal API keys |
|
||||
| **Who can edit** | Workspace admins | Only you |
|
||||
| **Auto-shared** | Yes — all members get access on creation | No — only you have access |
|
||||
|
||||
<Callout type="info">
|
||||
When a workspace and personal secret share the same key name, the **workspace secret takes precedence**.
|
||||
</Callout>
|
||||
|
||||
### Resolution Order
|
||||
|
||||
When a workflow runs, Sim resolves secrets in this order:
|
||||
|
||||
1. **Workspace secrets** are checked first
|
||||
2. **Personal secrets** are used as a fallback — from the user who triggered the run (manual) or the workflow owner (automated runs via API, webhook, or schedule)
|
||||
|
||||
## Access Control
|
||||
|
||||
Each credential has role-based access control:
|
||||
|
||||
- **Admin** — can view, edit, delete, and manage who has access
|
||||
- **Member** — can use the credential in workflows (read-only)
|
||||
|
||||
When you create a workspace secret, all current workspace members are automatically granted access. Personal secrets are only accessible to you by default.
|
||||
|
||||
### Sharing a Credential
|
||||
|
||||
To share a credential with specific team members:
|
||||
|
||||
1. Click **Details** on the credential
|
||||
2. Invite members by email
|
||||
3. Assign them an **Admin** or **Member** role
|
||||
|
||||
## Best Practices
|
||||
|
||||
- **Use workspace credentials for production** so workflows work regardless of who triggers them
|
||||
- **Use personal credentials for development** to keep your test keys separate
|
||||
- **Name keys descriptively** — `STRIPE_SECRET_KEY` over `KEY1`
|
||||
- **Connect multiple OAuth accounts** when you need different permissions or identities per workflow
|
||||
- **Never hardcode secrets** in workflow input fields — always use `{{KEY}}` references
|
||||
@@ -97,6 +97,7 @@ Understanding these core principles will help you build better workflows:
|
||||
3. **Smart Data Flow**: Outputs flow automatically to connected blocks
|
||||
4. **Error Handling**: Failed blocks stop their execution path but don't affect independent paths
|
||||
5. **State Persistence**: All block outputs and execution details are preserved for debugging
|
||||
6. **Cycle Protection**: Workflows that call other workflows (via Workflow blocks, MCP tools, or API blocks) are tracked with a call chain. If the chain exceeds 25 hops, execution is stopped to prevent infinite loops
|
||||
|
||||
## Next Steps
|
||||
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
"skills",
|
||||
"knowledgebase",
|
||||
"variables",
|
||||
"credentials",
|
||||
"execution",
|
||||
"permissions",
|
||||
"sdks",
|
||||
|
||||
1046
apps/docs/content/docs/en/tools/attio.mdx
Normal file
1046
apps/docs/content/docs/en/tools/attio.mdx
Normal file
File diff suppressed because it is too large
Load Diff
@@ -10,6 +10,7 @@
|
||||
"apollo",
|
||||
"arxiv",
|
||||
"asana",
|
||||
"attio",
|
||||
"browser_use",
|
||||
"calcom",
|
||||
"calendly",
|
||||
@@ -145,4 +146,4 @@
|
||||
"zep",
|
||||
"zoom"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -1,96 +0,0 @@
|
||||
---
|
||||
title: Environment Variables
|
||||
---
|
||||
|
||||
import { Callout } from 'fumadocs-ui/components/callout'
|
||||
import { Image } from '@/components/ui/image'
|
||||
|
||||
Environment variables provide a secure way to manage configuration values and secrets across your workflows, including API keys and other sensitive data that your workflows need to access. They keep secrets out of your workflow definitions while making them available during execution.
|
||||
|
||||
## Variable Types
|
||||
|
||||
Environment variables in Sim work at two levels:
|
||||
|
||||
- **Personal Environment Variables**: Private to your account, only you can see and use them
|
||||
- **Workspace Environment Variables**: Shared across the entire workspace, available to all team members
|
||||
|
||||
<Callout type="info">
|
||||
Workspace environment variables take precedence over personal ones when there's a naming conflict.
|
||||
</Callout>
|
||||
|
||||
## Setting up Environment Variables
|
||||
|
||||
Navigate to Settings to configure your environment variables:
|
||||
|
||||
<Image
|
||||
src="/static/environment/environment-1.png"
|
||||
alt="Environment variables modal for creating new variables"
|
||||
width={500}
|
||||
height={350}
|
||||
/>
|
||||
|
||||
From your workspace settings, you can create and manage both personal and workspace-level environment variables. Personal variables are private to your account, while workspace variables are shared with all team members.
|
||||
|
||||
### Making Variables Workspace-Scoped
|
||||
|
||||
Use the workspace scope toggle to make variables available to your entire team:
|
||||
|
||||
<Image
|
||||
src="/static/environment/environment-2.png"
|
||||
alt="Toggle workspace scope for environment variables"
|
||||
width={500}
|
||||
height={350}
|
||||
/>
|
||||
|
||||
When you enable workspace scope, the variable becomes available to all workspace members and can be used in any workflow within that workspace.
|
||||
|
||||
### Workspace Variables View
|
||||
|
||||
Once you have workspace-scoped variables, they appear in your environment variables list:
|
||||
|
||||
<Image
|
||||
src="/static/environment/environment-3.png"
|
||||
alt="Workspace-scoped variables in the environment variables list"
|
||||
width={500}
|
||||
height={350}
|
||||
/>
|
||||
|
||||
## Using Variables in Workflows
|
||||
|
||||
To reference environment variables in your workflows, use the `{{}}` notation. When you type `{{` in any input field, a dropdown will appear showing both your personal and workspace-level environment variables. Simply select the variable you want to use.
|
||||
|
||||
<Image
|
||||
src="/static/environment/environment-4.png"
|
||||
alt="Using environment variables with double brace notation"
|
||||
width={500}
|
||||
height={350}
|
||||
/>
|
||||
|
||||
## How Variables are Resolved
|
||||
|
||||
**Workspace variables always take precedence** over personal variables, regardless of who runs the workflow.
|
||||
|
||||
When no workspace variable exists for a key, personal variables are used:
|
||||
- **Manual runs (UI)**: Your personal variables
|
||||
- **Automated runs (API, webhook, schedule, deployed chat)**: Workflow owner's personal variables
|
||||
|
||||
<Callout type="info">
|
||||
Personal variables are best for testing. Use workspace variables for production workflows.
|
||||
</Callout>
|
||||
|
||||
## Security Best Practices
|
||||
|
||||
### For Sensitive Data
|
||||
- Store API keys, tokens, and passwords as environment variables instead of hardcoding them
|
||||
- Use workspace variables for shared resources that multiple team members need
|
||||
- Keep personal credentials in personal variables
|
||||
|
||||
### Variable Naming
|
||||
- Use descriptive names: `DATABASE_URL` instead of `DB`
|
||||
- Follow consistent naming conventions across your team
|
||||
- Consider prefixes to avoid conflicts: `PROD_API_KEY`, `DEV_API_KEY`
|
||||
|
||||
### Access Control
|
||||
- Workspace environment variables respect workspace permissions
|
||||
- Only users with write access or higher can create/modify workspace variables
|
||||
- Personal variables are always private to the individual user
|
||||
@@ -1,96 +0,0 @@
|
||||
---
|
||||
title: Variables de entorno
|
||||
---
|
||||
|
||||
import { Callout } from 'fumadocs-ui/components/callout'
|
||||
import { Image } from '@/components/ui/image'
|
||||
|
||||
Las variables de entorno proporcionan una forma segura de gestionar valores de configuración y secretos en tus flujos de trabajo, incluyendo claves API y otros datos sensibles que tus flujos de trabajo necesitan acceder. Mantienen los secretos fuera de las definiciones de tu flujo de trabajo mientras los hacen disponibles durante la ejecución.
|
||||
|
||||
## Tipos de variables
|
||||
|
||||
Las variables de entorno en Sim funcionan en dos niveles:
|
||||
|
||||
- **Variables de entorno personales**: Privadas para tu cuenta, solo tú puedes verlas y usarlas
|
||||
- **Variables de entorno del espacio de trabajo**: Compartidas en todo el espacio de trabajo, disponibles para todos los miembros del equipo
|
||||
|
||||
<Callout type="info">
|
||||
Las variables de entorno del espacio de trabajo tienen prioridad sobre las personales cuando hay un conflicto de nombres.
|
||||
</Callout>
|
||||
|
||||
## Configuración de variables de entorno
|
||||
|
||||
Navega a Configuración para configurar tus variables de entorno:
|
||||
|
||||
<Image
|
||||
src="/static/environment/environment-1.png"
|
||||
alt="Modal de variables de entorno para crear nuevas variables"
|
||||
width={500}
|
||||
height={350}
|
||||
/>
|
||||
|
||||
Desde la configuración de tu espacio de trabajo, puedes crear y gestionar variables de entorno tanto personales como a nivel de espacio de trabajo. Las variables personales son privadas para tu cuenta, mientras que las variables del espacio de trabajo se comparten con todos los miembros del equipo.
|
||||
|
||||
### Hacer variables con ámbito de espacio de trabajo
|
||||
|
||||
Usa el interruptor de ámbito del espacio de trabajo para hacer que las variables estén disponibles para todo tu equipo:
|
||||
|
||||
<Image
|
||||
src="/static/environment/environment-2.png"
|
||||
alt="Interruptor de ámbito del espacio de trabajo para variables de entorno"
|
||||
width={500}
|
||||
height={350}
|
||||
/>
|
||||
|
||||
Cuando habilitas el ámbito del espacio de trabajo, la variable se vuelve disponible para todos los miembros del espacio de trabajo y puede ser utilizada en cualquier flujo de trabajo dentro de ese espacio de trabajo.
|
||||
|
||||
### Vista de variables del espacio de trabajo
|
||||
|
||||
Una vez que tienes variables con ámbito de espacio de trabajo, aparecen en tu lista de variables de entorno:
|
||||
|
||||
<Image
|
||||
src="/static/environment/environment-3.png"
|
||||
alt="Variables con ámbito de espacio de trabajo en la lista de variables de entorno"
|
||||
width={500}
|
||||
height={350}
|
||||
/>
|
||||
|
||||
## Uso de variables en flujos de trabajo
|
||||
|
||||
Para hacer referencia a variables de entorno en tus flujos de trabajo, utiliza la notación `{{}}`. Cuando escribas `{{` en cualquier campo de entrada, aparecerá un menú desplegable mostrando tanto tus variables de entorno personales como las del espacio de trabajo. Simplemente selecciona la variable que deseas utilizar.
|
||||
|
||||
<Image
|
||||
src="/static/environment/environment-4.png"
|
||||
alt="Uso de variables de entorno con notación de doble llave"
|
||||
width={500}
|
||||
height={350}
|
||||
/>
|
||||
|
||||
## Cómo se resuelven las variables
|
||||
|
||||
**Las variables del espacio de trabajo siempre tienen prioridad** sobre las variables personales, independientemente de quién ejecute el flujo de trabajo.
|
||||
|
||||
Cuando no existe una variable de espacio de trabajo para una clave, se utilizan las variables personales:
|
||||
- **Ejecuciones manuales (UI)**: Tus variables personales
|
||||
- **Ejecuciones automatizadas (API, webhook, programación, chat implementado)**: Variables personales del propietario del flujo de trabajo
|
||||
|
||||
<Callout type="info">
|
||||
Las variables personales son mejores para pruebas. Usa variables de espacio de trabajo para flujos de trabajo de producción.
|
||||
</Callout>
|
||||
|
||||
## Mejores prácticas de seguridad
|
||||
|
||||
### Para datos sensibles
|
||||
- Almacena claves API, tokens y contraseñas como variables de entorno en lugar de codificarlos directamente
|
||||
- Usa variables de espacio de trabajo para recursos compartidos que varios miembros del equipo necesitan
|
||||
- Mantén las credenciales personales en variables personales
|
||||
|
||||
### Nomenclatura de variables
|
||||
- Usa nombres descriptivos: `DATABASE_URL` en lugar de `DB`
|
||||
- Sigue convenciones de nomenclatura consistentes en todo tu equipo
|
||||
- Considera usar prefijos para evitar conflictos: `PROD_API_KEY`, `DEV_API_KEY`
|
||||
|
||||
### Control de acceso
|
||||
- Las variables de entorno del espacio de trabajo respetan los permisos del espacio de trabajo
|
||||
- Solo los usuarios con acceso de escritura o superior pueden crear/modificar variables del espacio de trabajo
|
||||
- Las variables personales siempre son privadas para el usuario individual
|
||||
@@ -1,96 +0,0 @@
|
||||
---
|
||||
title: Variables d'environnement
|
||||
---
|
||||
|
||||
import { Callout } from 'fumadocs-ui/components/callout'
|
||||
import { Image } from '@/components/ui/image'
|
||||
|
||||
Les variables d'environnement offrent un moyen sécurisé de gérer les valeurs de configuration et les secrets dans vos workflows, y compris les clés API et autres données sensibles dont vos workflows ont besoin. Elles gardent les secrets en dehors de vos définitions de workflow tout en les rendant disponibles pendant l'exécution.
|
||||
|
||||
## Types de variables
|
||||
|
||||
Les variables d'environnement dans Sim fonctionnent à deux niveaux :
|
||||
|
||||
- **Variables d'environnement personnelles** : privées à votre compte, vous seul pouvez les voir et les utiliser
|
||||
- **Variables d'environnement d'espace de travail** : partagées dans tout l'espace de travail, disponibles pour tous les membres de l'équipe
|
||||
|
||||
<Callout type="info">
|
||||
Les variables d'environnement d'espace de travail ont priorité sur les variables personnelles en cas de conflit de noms.
|
||||
</Callout>
|
||||
|
||||
## Configuration des variables d'environnement
|
||||
|
||||
Accédez aux Paramètres pour configurer vos variables d'environnement :
|
||||
|
||||
<Image
|
||||
src="/static/environment/environment-1.png"
|
||||
alt="Fenêtre modale de variables d'environnement pour créer de nouvelles variables"
|
||||
width={500}
|
||||
height={350}
|
||||
/>
|
||||
|
||||
Depuis les paramètres de votre espace de travail, vous pouvez créer et gérer des variables d'environnement personnelles et au niveau de l'espace de travail. Les variables personnelles sont privées à votre compte, tandis que les variables d'espace de travail sont partagées avec tous les membres de l'équipe.
|
||||
|
||||
### Définir des variables au niveau de l'espace de travail
|
||||
|
||||
Utilisez le bouton de portée d'espace de travail pour rendre les variables disponibles à toute votre équipe :
|
||||
|
||||
<Image
|
||||
src="/static/environment/environment-2.png"
|
||||
alt="Activer la portée d'espace de travail pour les variables d'environnement"
|
||||
width={500}
|
||||
height={350}
|
||||
/>
|
||||
|
||||
Lorsque vous activez la portée d'espace de travail, la variable devient disponible pour tous les membres de l'espace de travail et peut être utilisée dans n'importe quel workflow au sein de cet espace de travail.
|
||||
|
||||
### Vue des variables d'espace de travail
|
||||
|
||||
Une fois que vous avez des variables à portée d'espace de travail, elles apparaissent dans votre liste de variables d'environnement :
|
||||
|
||||
<Image
|
||||
src="/static/environment/environment-3.png"
|
||||
alt="Variables à portée d'espace de travail dans la liste des variables d'environnement"
|
||||
width={500}
|
||||
height={350}
|
||||
/>
|
||||
|
||||
## Utilisation des variables dans les workflows
|
||||
|
||||
Pour référencer des variables d'environnement dans vos workflows, utilisez la notation `{{}}`. Lorsque vous tapez `{{` dans n'importe quel champ de saisie, un menu déroulant apparaîtra affichant à la fois vos variables d'environnement personnelles et celles au niveau de l'espace de travail. Sélectionnez simplement la variable que vous souhaitez utiliser.
|
||||
|
||||
<Image
|
||||
src="/static/environment/environment-4.png"
|
||||
alt="Utilisation des variables d'environnement avec la notation à double accolade"
|
||||
width={500}
|
||||
height={350}
|
||||
/>
|
||||
|
||||
## Comment les variables sont résolues
|
||||
|
||||
**Les variables d'espace de travail ont toujours la priorité** sur les variables personnelles, quel que soit l'utilisateur qui exécute le flux de travail.
|
||||
|
||||
Lorsqu'aucune variable d'espace de travail n'existe pour une clé, les variables personnelles sont utilisées :
|
||||
- **Exécutions manuelles (UI)** : Vos variables personnelles
|
||||
- **Exécutions automatisées (API, webhook, planification, chat déployé)** : Variables personnelles du propriétaire du flux de travail
|
||||
|
||||
<Callout type="info">
|
||||
Les variables personnelles sont idéales pour les tests. Utilisez les variables d'espace de travail pour les flux de travail en production.
|
||||
</Callout>
|
||||
|
||||
## Bonnes pratiques de sécurité
|
||||
|
||||
### Pour les données sensibles
|
||||
- Stockez les clés API, les jetons et les mots de passe comme variables d'environnement au lieu de les coder en dur
|
||||
- Utilisez des variables d'espace de travail pour les ressources partagées dont plusieurs membres de l'équipe ont besoin
|
||||
- Conservez vos identifiants personnels dans des variables personnelles
|
||||
|
||||
### Nommage des variables
|
||||
- Utilisez des noms descriptifs : `DATABASE_URL` au lieu de `DB`
|
||||
- Suivez des conventions de nommage cohérentes au sein de votre équipe
|
||||
- Envisagez des préfixes pour éviter les conflits : `PROD_API_KEY`, `DEV_API_KEY`
|
||||
|
||||
### Contrôle d'accès
|
||||
- Les variables d'environnement de l'espace de travail respectent les permissions de l'espace de travail
|
||||
- Seuls les utilisateurs disposant d'un accès en écriture ou supérieur peuvent créer/modifier les variables d'espace de travail
|
||||
- Les variables personnelles sont toujours privées pour l'utilisateur individuel
|
||||
@@ -1,96 +0,0 @@
|
||||
---
|
||||
title: 環境変数
|
||||
---
|
||||
|
||||
import { Callout } from 'fumadocs-ui/components/callout'
|
||||
import { Image } from '@/components/ui/image'
|
||||
|
||||
環境変数は、APIキーやワークフローがアクセスする必要のあるその他の機密データなど、ワークフロー全体で設定値や機密情報を安全に管理する方法を提供します。これにより、実行中にそれらを利用可能にしながら、ワークフロー定義から機密情報を切り離すことができます。
|
||||
|
||||
## 変数タイプ
|
||||
|
||||
Simの環境変数は2つのレベルで機能します:
|
||||
|
||||
- **個人環境変数**:あなたのアカウントに限定され、あなただけが閲覧・使用できます
|
||||
- **ワークスペース環境変数**:ワークスペース全体で共有され、すべてのチームメンバーが利用できます
|
||||
|
||||
<Callout type="info">
|
||||
名前の競合がある場合、ワークスペース環境変数は個人環境変数よりも優先されます。
|
||||
</Callout>
|
||||
|
||||
## 環境変数の設定
|
||||
|
||||
設定に移動して環境変数を構成します:
|
||||
|
||||
<Image
|
||||
src="/static/environment/environment-1.png"
|
||||
alt="新しい変数を作成するための環境変数モーダル"
|
||||
width={500}
|
||||
height={350}
|
||||
/>
|
||||
|
||||
ワークスペース設定から、個人レベルとワークスペースレベルの両方の環境変数を作成・管理できます。個人変数はあなたのアカウントに限定されますが、ワークスペース変数はすべてのチームメンバーと共有されます。
|
||||
|
||||
### 変数をワークスペーススコープにする
|
||||
|
||||
ワークスペーススコープトグルを使用して、変数をチーム全体で利用可能にします:
|
||||
|
||||
<Image
|
||||
src="/static/environment/environment-2.png"
|
||||
alt="環境変数のワークスペーススコープを切り替えるトグル"
|
||||
width={500}
|
||||
height={350}
|
||||
/>
|
||||
|
||||
ワークスペーススコープを有効にすると、その変数はすべてのワークスペースメンバーが利用でき、そのワークスペース内のあらゆるワークフローで使用できるようになります。
|
||||
|
||||
### ワークスペース変数ビュー
|
||||
|
||||
ワークスペーススコープの変数を作成すると、環境変数リストに表示されます:
|
||||
|
||||
<Image
|
||||
src="/static/environment/environment-3.png"
|
||||
alt="環境変数リスト内のワークスペーススコープ変数"
|
||||
width={500}
|
||||
height={350}
|
||||
/>
|
||||
|
||||
## ワークフローでの変数の使用
|
||||
|
||||
ワークフローで環境変数を参照するには、`{{}}`表記を使用します。任意の入力フィールドで`{{`と入力すると、個人用とワークスペースレベルの両方の環境変数を表示するドロップダウンが表示されます。使用したい変数を選択するだけです。
|
||||
|
||||
<Image
|
||||
src="/static/environment/environment-4.png"
|
||||
alt="二重括弧表記を使用した環境変数の使用方法"
|
||||
width={500}
|
||||
height={350}
|
||||
/>
|
||||
|
||||
## 変数の解決方法
|
||||
|
||||
**ワークスペース変数は常に優先されます**。誰がワークフローを実行するかに関わらず、個人変数よりも優先されます。
|
||||
|
||||
キーに対するワークスペース変数が存在しない場合、個人変数が使用されます:
|
||||
- **手動実行(UI)**:あなたの個人変数
|
||||
- **自動実行(API、ウェブフック、スケジュール、デプロイされたチャット)**:ワークフロー所有者の個人変数
|
||||
|
||||
<Callout type="info">
|
||||
個人変数はテストに最適です。本番環境のワークフローにはワークスペース変数を使用してください。
|
||||
</Callout>
|
||||
|
||||
## セキュリティのベストプラクティス
|
||||
|
||||
### 機密データについて
|
||||
- APIキー、トークン、パスワードはハードコーディングせず、環境変数として保存してください
|
||||
- 複数のチームメンバーが必要とする共有リソースにはワークスペース変数を使用してください
|
||||
- 個人の認証情報は個人変数に保管してください
|
||||
|
||||
### 変数の命名
|
||||
- 説明的な名前を使用する:`DATABASE_URL`ではなく`DB`
|
||||
- チーム全体で一貫した命名規則に従う
|
||||
- 競合を避けるために接頭辞を検討する:`PROD_API_KEY`、`DEV_API_KEY`
|
||||
|
||||
### アクセス制御
|
||||
- ワークスペース環境変数はワークスペースの権限を尊重します
|
||||
- 書き込みアクセス権以上を持つユーザーのみがワークスペース変数を作成/変更できます
|
||||
- 個人変数は常に個々のユーザーにプライベートです
|
||||
@@ -1,96 +0,0 @@
|
||||
---
|
||||
title: 环境变量
|
||||
---
|
||||
|
||||
import { Callout } from 'fumadocs-ui/components/callout'
|
||||
import { Image } from '@/components/ui/image'
|
||||
|
||||
环境变量为管理工作流中的配置值和密钥(包括 API 密钥和其他敏感数据)提供了一种安全的方式。它们可以在执行期间使用,同时将敏感信息从工作流定义中隔离开来。
|
||||
|
||||
## 变量类型
|
||||
|
||||
Sim 中的环境变量分为两个级别:
|
||||
|
||||
- **个人环境变量**:仅限于您的账户,只有您可以查看和使用
|
||||
- **工作区环境变量**:在整个工作区内共享,所有团队成员都可以使用
|
||||
|
||||
<Callout type="info">
|
||||
当命名冲突时,工作区环境变量优先于个人环境变量。
|
||||
</Callout>
|
||||
|
||||
## 设置环境变量
|
||||
|
||||
前往设置页面配置您的环境变量:
|
||||
|
||||
<Image
|
||||
src="/static/environment/environment-1.png"
|
||||
alt="用于创建新变量的环境变量弹窗"
|
||||
width={500}
|
||||
height={350}
|
||||
/>
|
||||
|
||||
在工作区设置中,您可以创建和管理个人及工作区级别的环境变量。个人变量仅限于您的账户,而工作区变量会与所有团队成员共享。
|
||||
|
||||
### 将变量设为工作区范围
|
||||
|
||||
使用工作区范围切换按钮,使变量对整个团队可用:
|
||||
|
||||
<Image
|
||||
src="/static/environment/environment-2.png"
|
||||
alt="切换环境变量的工作区范围"
|
||||
width={500}
|
||||
height={350}
|
||||
/>
|
||||
|
||||
启用工作区范围后,该变量将对所有工作区成员可用,并可在该工作区内的任何工作流中使用。
|
||||
|
||||
### 工作区变量视图
|
||||
|
||||
一旦您拥有了工作区范围的变量,它们将显示在您的环境变量列表中:
|
||||
|
||||
<Image
|
||||
src="/static/environment/environment-3.png"
|
||||
alt="环境变量列表中的工作区范围变量"
|
||||
width={500}
|
||||
height={350}
|
||||
/>
|
||||
|
||||
## 在工作流中使用变量
|
||||
|
||||
要在工作流中引用环境变量,请使用 `{{}}` 表示法。当您在任何输入字段中键入 `{{` 时,将会出现一个下拉菜单,显示您的个人和工作区级别的环境变量。只需选择您想要使用的变量即可。
|
||||
|
||||
<Image
|
||||
src="/static/environment/environment-4.png"
|
||||
alt="使用双大括号表示法的环境变量"
|
||||
width={500}
|
||||
height={350}
|
||||
/>
|
||||
|
||||
## 变量的解析方式
|
||||
|
||||
**工作区变量始终优先于**个人变量,无论是谁运行工作流。
|
||||
|
||||
当某个键没有工作区变量时,将使用个人变量:
|
||||
- **手动运行(UI)**:使用您的个人变量
|
||||
- **自动运行(API、Webhook、计划任务、已部署的聊天)**:使用工作流所有者的个人变量
|
||||
|
||||
<Callout type="info">
|
||||
个人变量最适合用于测试。生产环境的工作流请使用工作区变量。
|
||||
</Callout>
|
||||
|
||||
## 安全最佳实践
|
||||
|
||||
### 针对敏感数据
|
||||
- 将 API 密钥、令牌和密码存储为环境变量,而不是硬编码它们
|
||||
- 对于多个团队成员需要的共享资源,使用工作区变量
|
||||
- 将个人凭据保存在个人变量中
|
||||
|
||||
### 变量命名
|
||||
- 使用描述性名称:`DATABASE_URL` 而不是 `DB`
|
||||
- 在团队中遵循一致的命名约定
|
||||
- 考虑使用前缀以避免冲突:`PROD_API_KEY`、`DEV_API_KEY`
|
||||
|
||||
### 访问控制
|
||||
- 工作区环境变量遵循工作区权限
|
||||
- 只有具有写入权限或更高权限的用户才能创建/修改工作区变量
|
||||
- 个人变量始终对个人用户私有
|
||||
BIN
apps/docs/public/static/credentials/create-oauth.png
Normal file
BIN
apps/docs/public/static/credentials/create-oauth.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 83 KiB |
BIN
apps/docs/public/static/credentials/create-secret.png
Normal file
BIN
apps/docs/public/static/credentials/create-secret.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 84 KiB |
BIN
apps/docs/public/static/credentials/oauth-selector.png
Normal file
BIN
apps/docs/public/static/credentials/oauth-selector.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 48 KiB |
BIN
apps/docs/public/static/credentials/secret-dropdown.png
Normal file
BIN
apps/docs/public/static/credentials/secret-dropdown.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 23 KiB |
BIN
apps/docs/public/static/credentials/secret-resolved.png
Normal file
BIN
apps/docs/public/static/credentials/secret-resolved.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 26 KiB |
BIN
apps/docs/public/static/credentials/settings-secrets.png
Normal file
BIN
apps/docs/public/static/credentials/settings-secrets.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 72 KiB |
@@ -3,6 +3,7 @@ import type { NextRequest } from 'next/server'
|
||||
import { getHighestPrioritySubscription } from '@/lib/billing/core/plan'
|
||||
import { getExecutionTimeout } from '@/lib/core/execution-limits'
|
||||
import type { SubscriptionPlan } from '@/lib/core/rate-limiter/types'
|
||||
import { SIM_VIA_HEADER } from '@/lib/execution/call-chain'
|
||||
import { getParsedBody, withMcpAuth } from '@/lib/mcp/middleware'
|
||||
import { mcpService } from '@/lib/mcp/service'
|
||||
import type { McpTool, McpToolCall, McpToolResult } from '@/lib/mcp/types'
|
||||
@@ -178,8 +179,14 @@ export const POST = withMcpAuth('read')(
|
||||
'sync'
|
||||
)
|
||||
|
||||
const simViaHeader = request.headers.get(SIM_VIA_HEADER)
|
||||
const extraHeaders: Record<string, string> = {}
|
||||
if (simViaHeader) {
|
||||
extraHeaders[SIM_VIA_HEADER] = simViaHeader
|
||||
}
|
||||
|
||||
const result = await Promise.race([
|
||||
mcpService.executeTool(userId, serverId, toolCall, workspaceId),
|
||||
mcpService.executeTool(userId, serverId, toolCall, workspaceId, extraHeaders),
|
||||
new Promise<never>((_, reject) =>
|
||||
setTimeout(() => reject(new Error('Tool execution timeout')), executionTimeout)
|
||||
),
|
||||
|
||||
@@ -3,8 +3,8 @@
|
||||
import type React from 'react'
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
import { motion } from 'framer-motion'
|
||||
import { AlertCircle, Paperclip, Send, Square, X } from 'lucide-react'
|
||||
import { Tooltip } from '@/components/emcn'
|
||||
import { Paperclip, Send, Square, X } from 'lucide-react'
|
||||
import { Badge, Tooltip } from '@/components/emcn'
|
||||
import { VoiceInput } from '@/app/chat/components/input/voice-input'
|
||||
|
||||
const logger = createLogger('ChatInput')
|
||||
@@ -218,24 +218,12 @@ export const ChatInput: React.FC<{
|
||||
<div ref={wrapperRef} className='w-full max-w-3xl md:max-w-[748px]'>
|
||||
{/* Error Messages */}
|
||||
{uploadErrors.length > 0 && (
|
||||
<div className='mb-3'>
|
||||
<div className='rounded-lg border border-red-200 bg-red-50 p-3 dark:border-red-800/50 dark:bg-red-950/20'>
|
||||
<div className='flex items-start gap-2'>
|
||||
<AlertCircle className='mt-0.5 h-4 w-4 shrink-0 text-red-600 dark:text-red-400' />
|
||||
<div className='flex-1'>
|
||||
<div className='mb-1 font-medium text-red-800 text-sm dark:text-red-300'>
|
||||
File upload error
|
||||
</div>
|
||||
<div className='space-y-1'>
|
||||
{uploadErrors.map((error, idx) => (
|
||||
<div key={idx} className='text-red-700 text-sm dark:text-red-400'>
|
||||
{error}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className='mb-3 flex flex-col gap-2'>
|
||||
{uploadErrors.map((error, idx) => (
|
||||
<Badge key={idx} variant='red' size='lg' dot className='max-w-full'>
|
||||
{error}
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
import { useCallback, useEffect, useState } from 'react'
|
||||
import { createLogger } from '@sim/logger'
|
||||
import {
|
||||
Badge,
|
||||
Button,
|
||||
Combobox,
|
||||
DatePicker,
|
||||
@@ -706,12 +707,10 @@ export function DocumentTagsModal({
|
||||
(def) =>
|
||||
def.displayName.toLowerCase() === editTagForm.displayName.toLowerCase()
|
||||
) && (
|
||||
<div className='rounded-[4px] border border-amber-500/50 bg-amber-500/10 p-[8px]'>
|
||||
<p className='text-[11px] text-amber-600 dark:text-amber-400'>
|
||||
Maximum tag definitions reached. You can still use existing tag
|
||||
definitions, but cannot create new ones.
|
||||
</p>
|
||||
</div>
|
||||
<Badge variant='amber' size='lg' dot className='max-w-full'>
|
||||
Maximum tag definitions reached. You can still use existing tag definitions,
|
||||
but cannot create new ones.
|
||||
</Badge>
|
||||
)}
|
||||
|
||||
<div className='flex gap-[8px]'>
|
||||
|
||||
@@ -301,6 +301,16 @@ const SCOPE_DESCRIPTIONS: Record<string, string> = {
|
||||
'user-follow-modify': 'Follow and unfollow artists and users',
|
||||
'user-read-playback-position': 'View playback position in podcasts',
|
||||
'ugc-image-upload': 'Upload images to Spotify playlists',
|
||||
// Attio
|
||||
'record_permission:read-write': 'Read and write CRM records',
|
||||
'object_configuration:read-write': 'Read and manage object schemas',
|
||||
'list_configuration:read-write': 'Read and manage list configurations',
|
||||
'list_entry:read-write': 'Read and write list entries',
|
||||
'note:read-write': 'Read and write notes',
|
||||
'task:read-write': 'Read and write tasks',
|
||||
'comment:read-write': 'Read and write comments and threads',
|
||||
'user_management:read': 'View workspace members',
|
||||
'webhook:read-write': 'Manage webhooks',
|
||||
}
|
||||
|
||||
function getScopeDescription(scope: string): string {
|
||||
|
||||
@@ -142,7 +142,7 @@ export function ApiKeys({ onOpenChange }: ApiKeysProps) {
|
||||
strokeWidth={2}
|
||||
/>
|
||||
<Input
|
||||
placeholder='Search API keys...'
|
||||
placeholder='Search Sim keys...'
|
||||
value={searchTerm}
|
||||
onChange={(e) => setSearchTerm(e.target.value)}
|
||||
className='h-auto flex-1 border-0 bg-transparent p-0 font-base leading-none placeholder:text-[var(--text-tertiary)] focus-visible:ring-0 focus-visible:ring-offset-0'
|
||||
@@ -195,7 +195,7 @@ export function ApiKeys({ onOpenChange }: ApiKeysProps) {
|
||||
</div>
|
||||
{workspaceKeys.length === 0 ? (
|
||||
<div className='text-[13px] text-[var(--text-muted)]'>
|
||||
No workspace API keys yet
|
||||
No workspace Sim keys yet
|
||||
</div>
|
||||
) : (
|
||||
workspaceKeys.map((key) => (
|
||||
@@ -301,7 +301,7 @@ export function ApiKeys({ onOpenChange }: ApiKeysProps) {
|
||||
</div>
|
||||
{isConflict && (
|
||||
<div className='text-[12px] text-[var(--text-error)] leading-tight'>
|
||||
Workspace API key with the same name overrides this. Rename your
|
||||
Workspace Sim key with the same name overrides this. Rename your
|
||||
personal key to use it.
|
||||
</div>
|
||||
)}
|
||||
@@ -317,7 +317,7 @@ export function ApiKeys({ onOpenChange }: ApiKeysProps) {
|
||||
filteredWorkspaceKeys.length === 0 &&
|
||||
(personalKeys.length > 0 || workspaceKeys.length > 0) && (
|
||||
<div className='py-[16px] text-center text-[13px] text-[var(--text-muted)]'>
|
||||
No API keys found matching "{searchTerm}"
|
||||
No Sim keys found matching "{searchTerm}"
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
@@ -331,7 +331,7 @@ export function ApiKeys({ onOpenChange }: ApiKeysProps) {
|
||||
<div className='mt-auto flex items-center justify-between'>
|
||||
<div className='flex items-center gap-[8px]'>
|
||||
<span className='font-medium text-[13px] text-[var(--text-secondary)]'>
|
||||
Allow personal API keys
|
||||
Allow personal Sim keys
|
||||
</span>
|
||||
<Tooltip.Root>
|
||||
<Tooltip.Trigger asChild>
|
||||
@@ -383,7 +383,7 @@ export function ApiKeys({ onOpenChange }: ApiKeysProps) {
|
||||
{/* Delete Confirmation Dialog */}
|
||||
<Modal open={showDeleteDialog} onOpenChange={setShowDeleteDialog}>
|
||||
<ModalContent size='sm'>
|
||||
<ModalHeader>Delete API key</ModalHeader>
|
||||
<ModalHeader>Delete Sim key</ModalHeader>
|
||||
<ModalBody>
|
||||
<p className='text-[12px] text-[var(--text-secondary)]'>
|
||||
Deleting{' '}
|
||||
|
||||
@@ -62,8 +62,8 @@ export function CreateApiKeyModal({
|
||||
if (isDuplicate) {
|
||||
setCreateError(
|
||||
keyType === 'workspace'
|
||||
? `A workspace API key named "${trimmedName}" already exists. Please choose a different name.`
|
||||
: `A personal API key named "${trimmedName}" already exists. Please choose a different name.`
|
||||
? `A workspace Sim key named "${trimmedName}" already exists. Please choose a different name.`
|
||||
: `A personal Sim key named "${trimmedName}" already exists. Please choose a different name.`
|
||||
)
|
||||
return
|
||||
}
|
||||
@@ -86,11 +86,11 @@ export function CreateApiKeyModal({
|
||||
} catch (error: unknown) {
|
||||
logger.error('API key creation failed:', { error })
|
||||
const errorMessage =
|
||||
error instanceof Error ? error.message : 'Failed to create API key. Please try again.'
|
||||
error instanceof Error ? error.message : 'Failed to create Sim key. Please try again.'
|
||||
if (errorMessage.toLowerCase().includes('already exists')) {
|
||||
setCreateError(errorMessage)
|
||||
} else {
|
||||
setCreateError('Failed to create API key. Please check your connection and try again.')
|
||||
setCreateError('Failed to create Sim key. Please check your connection and try again.')
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -113,7 +113,7 @@ export function CreateApiKeyModal({
|
||||
{/* Create API Key Dialog */}
|
||||
<Modal open={open} onOpenChange={onOpenChange}>
|
||||
<ModalContent size='sm'>
|
||||
<ModalHeader>Create new API key</ModalHeader>
|
||||
<ModalHeader>Create new Sim key</ModalHeader>
|
||||
<ModalBody>
|
||||
<p className='text-[12px] text-[var(--text-secondary)]'>
|
||||
{keyType === 'workspace'
|
||||
@@ -125,7 +125,7 @@ export function CreateApiKeyModal({
|
||||
{canManageWorkspaceKeys && (
|
||||
<div className='flex flex-col gap-[8px]'>
|
||||
<p className='font-medium text-[13px] text-[var(--text-secondary)]'>
|
||||
API Key Type
|
||||
Sim Key Type
|
||||
</p>
|
||||
<ButtonGroup
|
||||
value={keyType}
|
||||
@@ -143,7 +143,7 @@ export function CreateApiKeyModal({
|
||||
)}
|
||||
<div className='flex flex-col gap-[8px]'>
|
||||
<p className='font-medium text-[13px] text-[var(--text-secondary)]'>
|
||||
Enter a name for your API key to help you identify it later.
|
||||
Enter a name for your Sim key to help you identify it later.
|
||||
</p>
|
||||
{/* Hidden decoy fields to prevent browser autofill */}
|
||||
<input
|
||||
@@ -216,10 +216,10 @@ export function CreateApiKeyModal({
|
||||
}}
|
||||
>
|
||||
<ModalContent size='sm'>
|
||||
<ModalHeader>Your API key has been created</ModalHeader>
|
||||
<ModalHeader>Your Sim key has been created</ModalHeader>
|
||||
<ModalBody>
|
||||
<p className='text-[12px] text-[var(--text-secondary)]'>
|
||||
This is the only time you will see your API key.{' '}
|
||||
This is the only time you will see your Sim key.{' '}
|
||||
<span className='font-semibold text-[var(--text-primary)]'>
|
||||
Copy it now and store it securely.
|
||||
</span>
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -6,10 +6,10 @@ interface CredentialsProps {
|
||||
onOpenChange?: (open: boolean) => void
|
||||
}
|
||||
|
||||
export function Credentials(_props: CredentialsProps) {
|
||||
export function Credentials({ onOpenChange }: CredentialsProps) {
|
||||
return (
|
||||
<div className='h-full min-h-0'>
|
||||
<CredentialsManager />
|
||||
<CredentialsManager onOpenChange={onOpenChange} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -484,12 +484,12 @@ function ServerDetailView({ workspaceId, serverId, onBack }: ServerDetailViewPro
|
||||
{activeConfigTab === 'cursor' && (
|
||||
<a
|
||||
href={getCursorInstallUrl(server.isPublic, server.name)}
|
||||
className='absolute top-[6px] right-2'
|
||||
className='absolute top-[6px] right-2 inline-flex rounded-[6px] bg-[var(--surface-5)] ring-1 ring-[var(--border-1)] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--brand-primary)] focus-visible:ring-offset-2 focus-visible:ring-offset-[var(--surface-2)]'
|
||||
>
|
||||
<img
|
||||
src='https://cursor.com/deeplink/mcp-install-dark.svg'
|
||||
alt='Add to Cursor'
|
||||
className='h-[26px]'
|
||||
className='h-[26px] rounded-[6px] align-middle'
|
||||
/>
|
||||
</a>
|
||||
)}
|
||||
|
||||
@@ -19,7 +19,6 @@ import {
|
||||
} from 'lucide-react'
|
||||
import {
|
||||
Card,
|
||||
Connections,
|
||||
HexSimple,
|
||||
Key,
|
||||
SModal,
|
||||
@@ -32,6 +31,7 @@ import {
|
||||
SModalSidebarItem,
|
||||
SModalSidebarSection,
|
||||
SModalSidebarSectionTitle,
|
||||
TerminalWindow,
|
||||
} from '@/components/emcn'
|
||||
import { AgentSkillsIcon, McpIcon } from '@/components/icons'
|
||||
import { useSession } from '@/lib/auth/auth-client'
|
||||
@@ -153,11 +153,11 @@ const allNavigationItems: NavigationItem[] = [
|
||||
requiresHosted: true,
|
||||
requiresTeam: true,
|
||||
},
|
||||
{ id: 'credentials', label: 'Credentials', icon: Connections, section: 'account' },
|
||||
{ id: 'credentials', label: 'Secrets', icon: Key, section: 'account' },
|
||||
{ id: 'custom-tools', label: 'Custom Tools', icon: Wrench, section: 'tools' },
|
||||
{ id: 'skills', label: 'Skills', icon: AgentSkillsIcon, section: 'tools' },
|
||||
{ id: 'mcp', label: 'MCP Tools', icon: McpIcon, section: 'tools' },
|
||||
{ id: 'apikeys', label: 'API Keys', icon: Key, section: 'system' },
|
||||
{ id: 'apikeys', label: 'Sim Keys', icon: TerminalWindow, section: 'system' },
|
||||
{ id: 'workflow-mcp-servers', label: 'MCP Servers', icon: Server, section: 'system' },
|
||||
{
|
||||
id: 'byok',
|
||||
@@ -449,7 +449,18 @@ export function SettingsModal({ open, onOpenChange }: SettingsModalProps) {
|
||||
}
|
||||
}
|
||||
|
||||
const { hasUnsavedChanges, onCloseAttempt, setHasUnsavedChanges, setOnCloseAttempt } =
|
||||
useSettingsModalStore()
|
||||
|
||||
const handleDialogOpenChange = (newOpen: boolean) => {
|
||||
if (!newOpen && hasUnsavedChanges && onCloseAttempt) {
|
||||
onCloseAttempt()
|
||||
return
|
||||
}
|
||||
if (!newOpen) {
|
||||
setHasUnsavedChanges(false)
|
||||
setOnCloseAttempt(null)
|
||||
}
|
||||
onOpenChange(newOpen)
|
||||
}
|
||||
|
||||
@@ -461,7 +472,7 @@ export function SettingsModal({ open, onOpenChange }: SettingsModalProps) {
|
||||
</VisuallyHidden.Root>
|
||||
<VisuallyHidden.Root>
|
||||
<DialogPrimitive.Description>
|
||||
Configure your workspace settings, credentials, and preferences
|
||||
Configure your workspace settings, secrets, and preferences
|
||||
</DialogPrimitive.Description>
|
||||
</VisuallyHidden.Root>
|
||||
|
||||
|
||||
@@ -89,6 +89,38 @@ Example:
|
||||
'Request timeout in milliseconds (default: 300000 = 5 minutes, max: 600000 = 10 minutes)',
|
||||
mode: 'advanced',
|
||||
},
|
||||
{
|
||||
id: 'retries',
|
||||
title: 'Retries',
|
||||
type: 'short-input',
|
||||
placeholder: '0',
|
||||
description:
|
||||
'Number of retry attempts for timeouts, 429 responses, and 5xx errors (default: 0, no retries)',
|
||||
mode: 'advanced',
|
||||
},
|
||||
{
|
||||
id: 'retryDelayMs',
|
||||
title: 'Retry delay (ms)',
|
||||
type: 'short-input',
|
||||
placeholder: '500',
|
||||
description: 'Initial retry delay in milliseconds (exponential backoff)',
|
||||
mode: 'advanced',
|
||||
},
|
||||
{
|
||||
id: 'retryMaxDelayMs',
|
||||
title: 'Max retry delay (ms)',
|
||||
type: 'short-input',
|
||||
placeholder: '30000',
|
||||
description: 'Maximum delay between retries in milliseconds',
|
||||
mode: 'advanced',
|
||||
},
|
||||
{
|
||||
id: 'retryNonIdempotent',
|
||||
title: 'Retry non-idempotent methods',
|
||||
type: 'switch',
|
||||
description: 'Allow retries for POST/PATCH requests (may create duplicate requests)',
|
||||
mode: 'advanced',
|
||||
},
|
||||
],
|
||||
tools: {
|
||||
access: ['http_request'],
|
||||
@@ -100,6 +132,16 @@ Example:
|
||||
body: { type: 'json', description: 'Request body data' },
|
||||
params: { type: 'json', description: 'URL query parameters' },
|
||||
timeout: { type: 'number', description: 'Request timeout in milliseconds' },
|
||||
retries: { type: 'number', description: 'Number of retry attempts for retryable failures' },
|
||||
retryDelayMs: { type: 'number', description: 'Initial retry delay in milliseconds' },
|
||||
retryMaxDelayMs: {
|
||||
type: 'number',
|
||||
description: 'Maximum delay between retries in milliseconds',
|
||||
},
|
||||
retryNonIdempotent: {
|
||||
type: 'boolean',
|
||||
description: 'Allow retries for non-idempotent methods like POST/PATCH',
|
||||
},
|
||||
},
|
||||
outputs: {
|
||||
data: { type: 'json', description: 'API response data (JSON, text, or other formats)' },
|
||||
|
||||
1243
apps/sim/blocks/blocks/attio.ts
Normal file
1243
apps/sim/blocks/blocks/attio.ts
Normal file
File diff suppressed because it is too large
Load Diff
@@ -440,6 +440,36 @@ Return ONLY the range string - no sheet name, no explanations, no quotes.`,
|
||||
placeholder: 'Describe the range (e.g., "first 50 rows" or "column A")...',
|
||||
},
|
||||
},
|
||||
// Read Filter Fields (advanced mode only)
|
||||
{
|
||||
id: 'filterColumn',
|
||||
title: 'Filter Column',
|
||||
type: 'short-input',
|
||||
placeholder: 'Column header name to filter on (e.g., Email, Status)',
|
||||
condition: { field: 'operation', value: 'read' },
|
||||
mode: 'advanced',
|
||||
},
|
||||
{
|
||||
id: 'filterValue',
|
||||
title: 'Filter Value',
|
||||
type: 'short-input',
|
||||
placeholder: 'Value to match against',
|
||||
condition: { field: 'operation', value: 'read' },
|
||||
mode: 'advanced',
|
||||
},
|
||||
{
|
||||
id: 'filterMatchType',
|
||||
title: 'Match Type',
|
||||
type: 'dropdown',
|
||||
options: [
|
||||
{ label: 'Contains', id: 'contains' },
|
||||
{ label: 'Exact Match', id: 'exact' },
|
||||
{ label: 'Starts With', id: 'starts_with' },
|
||||
{ label: 'Ends With', id: 'ends_with' },
|
||||
],
|
||||
condition: { field: 'operation', value: 'read' },
|
||||
mode: 'advanced',
|
||||
},
|
||||
// Write-specific Fields
|
||||
{
|
||||
id: 'values',
|
||||
@@ -748,6 +778,9 @@ Return ONLY the JSON array - no explanations, no markdown, no extra text.`,
|
||||
batchData,
|
||||
sheetId,
|
||||
destinationSpreadsheetId,
|
||||
filterColumn,
|
||||
filterValue,
|
||||
filterMatchType,
|
||||
...rest
|
||||
} = params
|
||||
|
||||
@@ -836,6 +869,11 @@ Return ONLY the JSON array - no explanations, no markdown, no extra text.`,
|
||||
cellRange: cellRange ? (cellRange as string).trim() : undefined,
|
||||
values: parsedValues,
|
||||
oauthCredential,
|
||||
...(filterColumn ? { filterColumn: (filterColumn as string).trim() } : {}),
|
||||
...(filterValue !== undefined && filterValue !== ''
|
||||
? { filterValue: filterValue as string }
|
||||
: {}),
|
||||
...(filterMatchType ? { filterMatchType: filterMatchType as string } : {}),
|
||||
}
|
||||
},
|
||||
},
|
||||
@@ -858,6 +896,12 @@ Return ONLY the JSON array - no explanations, no markdown, no extra text.`,
|
||||
type: 'string',
|
||||
description: 'Destination spreadsheet ID for copy',
|
||||
},
|
||||
filterColumn: { type: 'string', description: 'Column header name to filter on' },
|
||||
filterValue: { type: 'string', description: 'Value to match against the filter column' },
|
||||
filterMatchType: {
|
||||
type: 'string',
|
||||
description: 'Match type: contains, exact, starts_with, or ends_with',
|
||||
},
|
||||
},
|
||||
outputs: {
|
||||
// Read outputs
|
||||
|
||||
@@ -10,6 +10,7 @@ import { ApifyBlock } from '@/blocks/blocks/apify'
|
||||
import { ApolloBlock } from '@/blocks/blocks/apollo'
|
||||
import { ArxivBlock } from '@/blocks/blocks/arxiv'
|
||||
import { AsanaBlock } from '@/blocks/blocks/asana'
|
||||
import { AttioBlock } from '@/blocks/blocks/attio'
|
||||
import { BrowserUseBlock } from '@/blocks/blocks/browser_use'
|
||||
import { CalComBlock } from '@/blocks/blocks/calcom'
|
||||
import { CalendlyBlock } from '@/blocks/blocks/calendly'
|
||||
@@ -187,6 +188,7 @@ export const registry: Record<string, BlockConfig> = {
|
||||
apollo: ApolloBlock,
|
||||
arxiv: ArxivBlock,
|
||||
asana: AsanaBlock,
|
||||
attio: AttioBlock,
|
||||
browser_use: BrowserUseBlock,
|
||||
calcom: CalComBlock,
|
||||
calendly: CalendlyBlock,
|
||||
|
||||
@@ -24,6 +24,7 @@ export { PanelLeft } from './panel-left'
|
||||
export { Play, PlayOutline } from './play'
|
||||
export { Redo } from './redo'
|
||||
export { Rocket } from './rocket'
|
||||
export { TerminalWindow } from './terminal-window'
|
||||
export { Trash } from './trash'
|
||||
export { Trash2 } from './trash2'
|
||||
export { Undo } from './undo'
|
||||
|
||||
26
apps/sim/components/emcn/icons/terminal-window.tsx
Normal file
26
apps/sim/components/emcn/icons/terminal-window.tsx
Normal file
@@ -0,0 +1,26 @@
|
||||
import type { SVGProps } from 'react'
|
||||
|
||||
/**
|
||||
* Terminal window icon component
|
||||
* @param props - SVG properties including className, fill, etc.
|
||||
*/
|
||||
export function TerminalWindow(props: SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
<svg
|
||||
width='16'
|
||||
height='14'
|
||||
viewBox='0 0 16 14'
|
||||
fill='none'
|
||||
xmlns='http://www.w3.org/2000/svg'
|
||||
{...props}
|
||||
>
|
||||
<path
|
||||
d='M3 0C1.34315 0 0 1.34315 0 3V11C0 12.6569 1.34315 14 3 14H13C14.6569 14 16 12.6569 16 11V3C16 1.34315 14.6569 0 13 0H3ZM1 3C1 1.89543 1.89543 1 3 1H13C14.1046 1 15 1.89543 15 3V4H1V3ZM1 5H15V11C15 12.1046 14.1046 13 13 13H3C1.89543 13 1 12.1046 1 11V5Z'
|
||||
fill='currentColor'
|
||||
/>
|
||||
<circle cx='3.5' cy='2.5' r='0.75' fill='currentColor' />
|
||||
<circle cx='5.75' cy='2.5' r='0.75' fill='currentColor' />
|
||||
<circle cx='8' cy='2.5' r='0.75' fill='currentColor' />
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
@@ -3552,6 +3552,15 @@ export function TrelloIcon(props: SVGProps<SVGSVGElement>) {
|
||||
)
|
||||
}
|
||||
|
||||
export function AttioIcon(props: SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
<svg {...props} xmlns='http://www.w3.org/2000/svg' viewBox='0 0 60.9 50' fill='currentColor'>
|
||||
<path d='M60.3,34.8l-5.1-8.1c0,0,0,0,0,0L54.7,26c-0.8-1.2-2.1-1.9-3.5-1.9L43,24L42.5,25l-9.8,15.7l-0.5,0.9l4.1,6.6c0.8,1.2,2.1,1.9,3.5,1.9h11.5c1.4,0,2.8-0.7,3.5-1.9l0.4-0.6c0,0,0,0,0,0l5.1-8.2C61.1,37.9,61.1,36.2,60.3,34.8L60.3,34.8z M58.7,38.3l-5.1,8.2c0,0,0,0.1-0.1,0.1c-0.2,0.2-0.4,0.2-0.5,0.2c-0.1,0-0.4,0-0.6-0.3l-5.1-8.2c-0.1-0.1-0.1-0.2-0.2-0.3c0-0.1-0.1-0.2-0.1-0.3c-0.1-0.4-0.1-0.8,0-1.3c0.1-0.2,0.1-0.4,0.3-0.6l5.1-8.1c0,0,0,0,0,0c0.1-0.2,0.3-0.3,0.4-0.3c0.1,0,0.1,0,0.1,0c0,0,0,0,0.1,0c0.1,0,0.4,0,0.6,0.3l5.1,8.1C59.2,36.6,59.2,37.5,58.7,38.3L58.7,38.3z' />
|
||||
<path d='M45.2,15.1c0.8-1.3,0.8-3.1,0-4.4l-5.1-8.1l-0.4-0.7C38.9,0.7,37.6,0,36.2,0H24.7c-1.4,0-2.7,0.7-3.5,1.9L0.6,34.9C0.2,35.5,0,36.3,0,37c0,0.8,0.2,1.5,0.6,2.2l5.5,8.8C6.9,49.3,8.2,50,9.7,50h11.5c1.4,0,2.8-0.7,3.5-1.9l0.4-0.7c0,0,0,0,0,0c0,0,0,0,0,0l4.1-6.6l12.1-19.4L45.2,15.1L45.2,15.1z M44,13c0,0.4-0.1,0.8-0.4,1.2L23.5,46.4c-0.2,0.3-0.5,0.3-0.6,0.3c-0.1,0-0.4,0-0.6-0.3l-5.1-8.2c-0.5-0.7-0.5-1.7,0-2.4L37.4,3.6c0.2-0.3,0.5-0.3,0.6-0.3c0.1,0,0.4,0,0.6,0.3l5.1,8.1C43.9,12.1,44,12.5,44,13z' />
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
export function AsanaIcon(props: SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
<svg {...props} xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none'>
|
||||
|
||||
@@ -158,7 +158,6 @@ export const DEFAULTS = {
|
||||
MAX_LOOP_ITERATIONS: 1000,
|
||||
MAX_FOREACH_ITEMS: 1000,
|
||||
MAX_PARALLEL_BRANCHES: 20,
|
||||
MAX_WORKFLOW_DEPTH: 10,
|
||||
MAX_SSE_CHILD_DEPTH: 3,
|
||||
EXECUTION_TIME: 0,
|
||||
TOKENS: {
|
||||
|
||||
@@ -123,7 +123,6 @@ describe('AgentBlockHandler', () => {
|
||||
let handler: AgentBlockHandler
|
||||
let mockBlock: SerializedBlock
|
||||
let mockContext: ExecutionContext
|
||||
let originalPromiseAll: any
|
||||
|
||||
beforeEach(() => {
|
||||
handler = new AgentBlockHandler()
|
||||
@@ -135,8 +134,6 @@ describe('AgentBlockHandler', () => {
|
||||
configurable: true,
|
||||
})
|
||||
|
||||
originalPromiseAll = Promise.all
|
||||
|
||||
mockBlock = {
|
||||
id: 'test-agent-block',
|
||||
metadata: { id: BlockType.AGENT, name: 'Test Agent' },
|
||||
@@ -209,8 +206,6 @@ describe('AgentBlockHandler', () => {
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
Promise.all = originalPromiseAll
|
||||
|
||||
try {
|
||||
Object.defineProperty(global, 'window', {
|
||||
value: undefined,
|
||||
@@ -271,38 +266,7 @@ describe('AgentBlockHandler', () => {
|
||||
expect(result).toEqual(expectedOutput)
|
||||
})
|
||||
|
||||
it('should preserve executeFunction for custom tools with different usageControl settings', async () => {
|
||||
let capturedTools: any[] = []
|
||||
|
||||
Promise.all = vi.fn().mockImplementation((promises: Promise<any>[]) => {
|
||||
const result = originalPromiseAll.call(Promise, promises)
|
||||
|
||||
result.then((tools: any[]) => {
|
||||
if (tools?.length) {
|
||||
capturedTools = tools.filter((t) => t !== null)
|
||||
}
|
||||
})
|
||||
|
||||
return result
|
||||
})
|
||||
|
||||
mockExecuteProviderRequest.mockResolvedValueOnce({
|
||||
content: 'Using tools to respond',
|
||||
model: 'mock-model',
|
||||
tokens: { input: 10, output: 20, total: 30 },
|
||||
toolCalls: [
|
||||
{
|
||||
name: 'auto_tool',
|
||||
arguments: { input: 'test input for auto tool' },
|
||||
},
|
||||
{
|
||||
name: 'force_tool',
|
||||
arguments: { input: 'test input for force tool' },
|
||||
},
|
||||
],
|
||||
timing: { total: 100 },
|
||||
})
|
||||
|
||||
it('should preserve usageControl for custom tools and filter out "none"', async () => {
|
||||
const inputs = {
|
||||
model: 'gpt-4o',
|
||||
userPrompt: 'Test custom tools with different usageControl settings',
|
||||
@@ -372,13 +336,14 @@ describe('AgentBlockHandler', () => {
|
||||
|
||||
await handler.execute(mockContext, mockBlock, inputs)
|
||||
|
||||
expect(Promise.all).toHaveBeenCalled()
|
||||
const providerCall = mockExecuteProviderRequest.mock.calls[0]
|
||||
const tools = providerCall[1].tools
|
||||
|
||||
expect(capturedTools.length).toBe(2)
|
||||
expect(tools.length).toBe(2)
|
||||
|
||||
const autoTool = capturedTools.find((t) => t.name === 'auto_tool')
|
||||
const forceTool = capturedTools.find((t) => t.name === 'force_tool')
|
||||
const noneTool = capturedTools.find((t) => t.name === 'none_tool')
|
||||
const autoTool = tools.find((t: any) => t.name === 'auto_tool')
|
||||
const forceTool = tools.find((t: any) => t.name === 'force_tool')
|
||||
const noneTool = tools.find((t: any) => t.name === 'none_tool')
|
||||
|
||||
expect(autoTool).toBeDefined()
|
||||
expect(forceTool).toBeDefined()
|
||||
@@ -386,37 +351,6 @@ describe('AgentBlockHandler', () => {
|
||||
|
||||
expect(autoTool.usageControl).toBe('auto')
|
||||
expect(forceTool.usageControl).toBe('force')
|
||||
|
||||
expect(typeof autoTool.executeFunction).toBe('function')
|
||||
expect(typeof forceTool.executeFunction).toBe('function')
|
||||
|
||||
await autoTool.executeFunction({ input: 'test input' })
|
||||
expect(mockExecuteTool).toHaveBeenCalledWith(
|
||||
'function_execute',
|
||||
expect.objectContaining({
|
||||
code: 'return { result: "auto tool executed", input }',
|
||||
input: 'test input',
|
||||
}),
|
||||
false, // skipPostProcess
|
||||
expect.any(Object) // execution context
|
||||
)
|
||||
|
||||
await forceTool.executeFunction({ input: 'another test' })
|
||||
expect(mockExecuteTool).toHaveBeenNthCalledWith(
|
||||
2, // Check the 2nd call
|
||||
'function_execute',
|
||||
expect.objectContaining({
|
||||
code: 'return { result: "force tool executed", input }',
|
||||
input: 'another test',
|
||||
}),
|
||||
false, // skipPostProcess
|
||||
expect.any(Object) // execution context
|
||||
)
|
||||
|
||||
const providerCall = mockExecuteProviderRequest.mock.calls[0]
|
||||
const requestBody = providerCall[1]
|
||||
|
||||
expect(requestBody.tools.length).toBe(2)
|
||||
})
|
||||
|
||||
it('should filter out tools with usageControl set to "none"', async () => {
|
||||
@@ -1763,6 +1697,52 @@ describe('AgentBlockHandler', () => {
|
||||
expect(providerCallArgs[1].tools[0].name).toBe('search_files')
|
||||
})
|
||||
|
||||
it('should pass callChain to executeProviderRequest for MCP cycle detection', async () => {
|
||||
mockFetch.mockImplementation(() =>
|
||||
Promise.resolve({ ok: true, json: () => Promise.resolve({}) })
|
||||
)
|
||||
|
||||
const inputs = {
|
||||
model: 'gpt-4o',
|
||||
userPrompt: 'Search for files',
|
||||
apiKey: 'test-api-key',
|
||||
tools: [
|
||||
{
|
||||
type: 'mcp',
|
||||
title: 'search_files',
|
||||
schema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
query: { type: 'string', description: 'Search query' },
|
||||
},
|
||||
required: ['query'],
|
||||
},
|
||||
params: {
|
||||
serverId: 'mcp-search-server',
|
||||
toolName: 'search_files',
|
||||
serverName: 'search',
|
||||
},
|
||||
usageControl: 'auto' as const,
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
const contextWithCallChain = {
|
||||
...mockContext,
|
||||
workspaceId: 'test-workspace-123',
|
||||
workflowId: 'test-workflow-456',
|
||||
callChain: ['wf-parent', 'test-workflow-456'],
|
||||
}
|
||||
|
||||
mockGetProviderFromModel.mockReturnValue('openai')
|
||||
|
||||
await handler.execute(contextWithCallChain, mockBlock, inputs)
|
||||
|
||||
expect(mockExecuteProviderRequest).toHaveBeenCalled()
|
||||
const providerCallArgs = mockExecuteProviderRequest.mock.calls[0][1]
|
||||
expect(providerCallArgs.callChain).toEqual(['wf-parent', 'test-workflow-456'])
|
||||
})
|
||||
|
||||
it('should handle multiple MCP tools from the same server efficiently', async () => {
|
||||
const fetchCalls: any[] = []
|
||||
|
||||
@@ -2139,21 +2119,10 @@ describe('AgentBlockHandler', () => {
|
||||
expect(tools.length).toBe(0)
|
||||
})
|
||||
|
||||
it('should use DB code for executeFunction when customToolId resolves', async () => {
|
||||
it('should use DB schema when customToolId resolves', async () => {
|
||||
const toolId = 'custom-tool-123'
|
||||
mockFetchForCustomTool(toolId)
|
||||
|
||||
let capturedTools: any[] = []
|
||||
Promise.all = vi.fn().mockImplementation((promises: Promise<any>[]) => {
|
||||
const result = originalPromiseAll.call(Promise, promises)
|
||||
result.then((tools: any[]) => {
|
||||
if (tools?.length) {
|
||||
capturedTools = tools.filter((t) => t !== null)
|
||||
}
|
||||
})
|
||||
return result
|
||||
})
|
||||
|
||||
const inputs = {
|
||||
model: 'gpt-4o',
|
||||
userPrompt: 'Format a report',
|
||||
@@ -2174,19 +2143,12 @@ describe('AgentBlockHandler', () => {
|
||||
|
||||
await handler.execute(mockContext, mockBlock, inputs)
|
||||
|
||||
expect(capturedTools.length).toBe(1)
|
||||
expect(typeof capturedTools[0].executeFunction).toBe('function')
|
||||
expect(mockExecuteProviderRequest).toHaveBeenCalled()
|
||||
const providerCall = mockExecuteProviderRequest.mock.calls[0]
|
||||
const tools = providerCall[1].tools
|
||||
|
||||
await capturedTools[0].executeFunction({ title: 'Q1', format: 'pdf' })
|
||||
|
||||
expect(mockExecuteTool).toHaveBeenCalledWith(
|
||||
'function_execute',
|
||||
expect.objectContaining({
|
||||
code: dbCode,
|
||||
}),
|
||||
false,
|
||||
expect.any(Object)
|
||||
)
|
||||
expect(tools.length).toBe(1)
|
||||
expect(tools[0].name).toBe('formatReport')
|
||||
})
|
||||
|
||||
it('should not fetch from DB when no customToolId is present', async () => {
|
||||
|
||||
@@ -33,7 +33,6 @@ import { stringifyJSON } from '@/executor/utils/json'
|
||||
import { executeProviderRequest } from '@/providers'
|
||||
import { getProviderFromModel, transformBlockTool } from '@/providers/utils'
|
||||
import type { SerializedBlock } from '@/serializer/types'
|
||||
import { executeTool } from '@/tools'
|
||||
import { getTool, getToolAsync } from '@/tools/utils'
|
||||
|
||||
const logger = createLogger('AgentBlockHandler')
|
||||
@@ -276,14 +275,12 @@ export class AgentBlockHandler implements BlockHandler {
|
||||
const userProvidedParams = tool.params || {}
|
||||
|
||||
let schema = tool.schema
|
||||
let code = tool.code
|
||||
let title = tool.title
|
||||
|
||||
if (tool.customToolId) {
|
||||
const resolved = await this.fetchCustomToolById(ctx, tool.customToolId)
|
||||
if (resolved) {
|
||||
schema = resolved.schema
|
||||
code = resolved.code
|
||||
title = resolved.title
|
||||
} else if (!schema) {
|
||||
logger.error(`Custom tool not found: ${tool.customToolId}`)
|
||||
@@ -296,7 +293,7 @@ export class AgentBlockHandler implements BlockHandler {
|
||||
return null
|
||||
}
|
||||
|
||||
const { filterSchemaForLLM, mergeToolParameters } = await import('@/tools/params')
|
||||
const { filterSchemaForLLM } = await import('@/tools/params')
|
||||
|
||||
const filteredSchema = filterSchemaForLLM(schema.function.parameters, userProvidedParams)
|
||||
|
||||
@@ -313,43 +310,6 @@ export class AgentBlockHandler implements BlockHandler {
|
||||
usageControl: tool.usageControl || 'auto',
|
||||
}
|
||||
|
||||
if (code) {
|
||||
base.executeFunction = async (callParams: Record<string, any>) => {
|
||||
const mergedParams = mergeToolParameters(userProvidedParams, callParams)
|
||||
|
||||
const { blockData, blockNameMapping, blockOutputSchemas } = collectBlockData(ctx)
|
||||
|
||||
const result = await executeTool(
|
||||
'function_execute',
|
||||
{
|
||||
code,
|
||||
...mergedParams,
|
||||
timeout: tool.timeout ?? AGENT.DEFAULT_FUNCTION_TIMEOUT,
|
||||
envVars: ctx.environmentVariables || {},
|
||||
workflowVariables: ctx.workflowVariables || {},
|
||||
blockData,
|
||||
blockNameMapping,
|
||||
blockOutputSchemas,
|
||||
isCustomTool: true,
|
||||
_context: {
|
||||
workflowId: ctx.workflowId,
|
||||
workspaceId: ctx.workspaceId,
|
||||
userId: ctx.userId,
|
||||
isDeployedContext: ctx.isDeployedContext,
|
||||
enforceCredentialAccess: ctx.enforceCredentialAccess,
|
||||
},
|
||||
},
|
||||
false,
|
||||
ctx
|
||||
)
|
||||
|
||||
if (!result.success) {
|
||||
throw new Error(result.error || 'Function execution failed')
|
||||
}
|
||||
return result.output
|
||||
}
|
||||
}
|
||||
|
||||
return base
|
||||
}
|
||||
|
||||
@@ -359,7 +319,7 @@ export class AgentBlockHandler implements BlockHandler {
|
||||
private async fetchCustomToolById(
|
||||
ctx: ExecutionContext,
|
||||
customToolId: string
|
||||
): Promise<{ schema: any; code: string; title: string } | null> {
|
||||
): Promise<{ schema: any; title: string } | null> {
|
||||
if (typeof window !== 'undefined') {
|
||||
try {
|
||||
const { getCustomTool } = await import('@/hooks/queries/custom-tools')
|
||||
@@ -367,7 +327,6 @@ export class AgentBlockHandler implements BlockHandler {
|
||||
if (tool) {
|
||||
return {
|
||||
schema: tool.schema,
|
||||
code: tool.code || '',
|
||||
title: tool.title,
|
||||
}
|
||||
}
|
||||
@@ -416,7 +375,6 @@ export class AgentBlockHandler implements BlockHandler {
|
||||
|
||||
return {
|
||||
schema: tool.schema,
|
||||
code: tool.code || '',
|
||||
title: tool.title,
|
||||
}
|
||||
} catch (error) {
|
||||
@@ -498,47 +456,6 @@ export class AgentBlockHandler implements BlockHandler {
|
||||
parameters: filteredSchema,
|
||||
params: userProvidedParams,
|
||||
usageControl: tool.usageControl || 'auto',
|
||||
executeFunction: async (callParams: Record<string, any>) => {
|
||||
const headers = await buildAuthHeaders()
|
||||
const execParams: Record<string, string> = {}
|
||||
if (ctx.userId) execParams.userId = ctx.userId
|
||||
const execUrl = buildAPIUrl('/api/mcp/tools/execute', execParams)
|
||||
|
||||
const execResponse = await fetch(execUrl.toString(), {
|
||||
method: 'POST',
|
||||
headers,
|
||||
body: stringifyJSON({
|
||||
serverId,
|
||||
toolName,
|
||||
arguments: callParams,
|
||||
workspaceId: ctx.workspaceId,
|
||||
workflowId: ctx.workflowId,
|
||||
toolSchema: tool.schema,
|
||||
}),
|
||||
})
|
||||
|
||||
if (!execResponse.ok) {
|
||||
throw new Error(
|
||||
`MCP tool execution failed: ${execResponse.status} ${execResponse.statusText}`
|
||||
)
|
||||
}
|
||||
|
||||
const result = await execResponse.json()
|
||||
if (!result.success) {
|
||||
throw new Error(result.error || 'MCP tool execution failed')
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: result.data.output || {},
|
||||
metadata: {
|
||||
source: 'mcp',
|
||||
serverId,
|
||||
serverName: serverName || serverId,
|
||||
toolName,
|
||||
},
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -684,47 +601,6 @@ export class AgentBlockHandler implements BlockHandler {
|
||||
parameters: filteredSchema,
|
||||
params: userProvidedParams,
|
||||
usageControl: tool.usageControl || 'auto',
|
||||
executeFunction: async (callParams: Record<string, any>) => {
|
||||
const headers = await buildAuthHeaders()
|
||||
const discoverExecParams: Record<string, string> = {}
|
||||
if (ctx.userId) discoverExecParams.userId = ctx.userId
|
||||
const execUrl = buildAPIUrl('/api/mcp/tools/execute', discoverExecParams)
|
||||
|
||||
const execResponse = await fetch(execUrl.toString(), {
|
||||
method: 'POST',
|
||||
headers,
|
||||
body: stringifyJSON({
|
||||
serverId,
|
||||
toolName,
|
||||
arguments: callParams,
|
||||
workspaceId: ctx.workspaceId,
|
||||
workflowId: ctx.workflowId,
|
||||
toolSchema: mcpTool.inputSchema,
|
||||
}),
|
||||
})
|
||||
|
||||
if (!execResponse.ok) {
|
||||
throw new Error(
|
||||
`MCP tool execution failed: ${execResponse.status} ${execResponse.statusText}`
|
||||
)
|
||||
}
|
||||
|
||||
const result = await execResponse.json()
|
||||
if (!result.success) {
|
||||
throw new Error(result.error || 'MCP tool execution failed')
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: result.data.output || {},
|
||||
metadata: {
|
||||
source: 'mcp',
|
||||
serverId,
|
||||
serverName: mcpTool.serverName,
|
||||
toolName,
|
||||
},
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1082,10 +958,12 @@ export class AgentBlockHandler implements BlockHandler {
|
||||
blockData,
|
||||
blockNameMapping,
|
||||
isDeployedContext: ctx.isDeployedContext,
|
||||
callChain: ctx.callChain,
|
||||
reasoningEffort: providerRequest.reasoningEffort,
|
||||
verbosity: providerRequest.verbosity,
|
||||
thinkingLevel: providerRequest.thinkingLevel,
|
||||
previousInteractionId: providerRequest.previousInteractionId,
|
||||
abortSignal: ctx.abortSignal,
|
||||
})
|
||||
|
||||
return this.processProviderResponse(response, block, responseFormat)
|
||||
|
||||
@@ -108,18 +108,16 @@ describe('WorkflowBlockHandler', () => {
|
||||
)
|
||||
})
|
||||
|
||||
it('should enforce maximum depth limit', async () => {
|
||||
it('should enforce maximum call chain depth limit', async () => {
|
||||
const inputs = { workflowId: 'child-workflow-id' }
|
||||
|
||||
// Create a deeply nested context (simulate 11 levels deep to exceed the limit of 10)
|
||||
const deepContext = {
|
||||
...mockContext,
|
||||
workflowId:
|
||||
'level1_sub_level2_sub_level3_sub_level4_sub_level5_sub_level6_sub_level7_sub_level8_sub_level9_sub_level10_sub_level11',
|
||||
callChain: Array.from({ length: 25 }, (_, i) => `wf-${i}`),
|
||||
}
|
||||
|
||||
await expect(handler.execute(deepContext, mockBlock, inputs)).rejects.toThrow(
|
||||
'"child-workflow-id" failed: Maximum workflow nesting depth of 10 exceeded'
|
||||
'Maximum workflow call chain depth (25) exceeded'
|
||||
)
|
||||
})
|
||||
|
||||
|
||||
@@ -98,13 +98,17 @@ export class WorkflowBlockHandler implements BlockHandler {
|
||||
// workflow block execution, preventing cross-iteration child mixing in loop contexts.
|
||||
const instanceId = crypto.randomUUID()
|
||||
|
||||
const childCallChain = buildNextCallChain(ctx.callChain || [], workflowId)
|
||||
const depthError = validateCallChain(childCallChain)
|
||||
if (depthError) {
|
||||
throw new ChildWorkflowError({
|
||||
message: depthError,
|
||||
childWorkflowName,
|
||||
})
|
||||
}
|
||||
|
||||
let childWorkflowSnapshotId: string | undefined
|
||||
try {
|
||||
const currentDepth = (ctx.workflowId?.split('_sub_').length || 1) - 1
|
||||
if (currentDepth >= DEFAULTS.MAX_WORKFLOW_DEPTH) {
|
||||
throw new Error(`Maximum workflow nesting depth of ${DEFAULTS.MAX_WORKFLOW_DEPTH} exceeded`)
|
||||
}
|
||||
|
||||
if (ctx.isDeployedContext) {
|
||||
const hasActiveDeployment = await this.checkChildDeployment(workflowId)
|
||||
if (!hasActiveDeployment) {
|
||||
@@ -126,7 +130,7 @@ export class WorkflowBlockHandler implements BlockHandler {
|
||||
childWorkflowName = workflowMetadata?.name || childWorkflow.name || 'Unknown Workflow'
|
||||
|
||||
logger.info(
|
||||
`Executing child workflow: ${childWorkflowName} (${workflowId}) at depth ${currentDepth}`
|
||||
`Executing child workflow: ${childWorkflowName} (${workflowId}), call chain depth ${ctx.callChain?.length || 0}`
|
||||
)
|
||||
|
||||
let childWorkflowInput: Record<string, any> = {}
|
||||
@@ -168,15 +172,6 @@ export class WorkflowBlockHandler implements BlockHandler {
|
||||
ctx.onChildWorkflowInstanceReady?.(effectiveBlockId, instanceId, iterationContext)
|
||||
}
|
||||
|
||||
const childCallChain = buildNextCallChain(ctx.callChain || [], workflowId)
|
||||
const depthError = validateCallChain(childCallChain)
|
||||
if (depthError) {
|
||||
throw new ChildWorkflowError({
|
||||
message: depthError,
|
||||
childWorkflowName,
|
||||
})
|
||||
}
|
||||
|
||||
const subExecutor = new Executor({
|
||||
workflow: childWorkflow.serializedState,
|
||||
workflowInput: childWorkflowInput,
|
||||
|
||||
@@ -503,6 +503,7 @@ export const auth = betterAuth({
|
||||
'zoom',
|
||||
'wordpress',
|
||||
'linear',
|
||||
'attio',
|
||||
'shopify',
|
||||
'trello',
|
||||
'calcom',
|
||||
@@ -2237,6 +2238,69 @@ export const auth = betterAuth({
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
providerId: 'attio',
|
||||
clientId: env.ATTIO_CLIENT_ID as string,
|
||||
clientSecret: env.ATTIO_CLIENT_SECRET as string,
|
||||
authorizationUrl: 'https://app.attio.com/authorize',
|
||||
tokenUrl: 'https://app.attio.com/oauth/token',
|
||||
scopes: [
|
||||
'record_permission:read-write',
|
||||
'object_configuration:read-write',
|
||||
'list_configuration:read-write',
|
||||
'list_entry:read-write',
|
||||
'note:read-write',
|
||||
'task:read-write',
|
||||
'comment:read-write',
|
||||
'user_management:read',
|
||||
'webhook:read-write',
|
||||
],
|
||||
responseType: 'code',
|
||||
redirectURI: `${getBaseUrl()}/api/auth/oauth2/callback/attio`,
|
||||
getUserInfo: async (tokens) => {
|
||||
try {
|
||||
const response = await fetch('https://api.attio.com/v2/workspace_members', {
|
||||
headers: {
|
||||
Authorization: `Bearer ${tokens.accessToken}`,
|
||||
},
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text()
|
||||
logger.error('Attio API error:', {
|
||||
status: response.status,
|
||||
statusText: response.statusText,
|
||||
body: errorText,
|
||||
})
|
||||
throw new Error(`Attio API error: ${response.status} ${response.statusText}`)
|
||||
}
|
||||
|
||||
const { data } = await response.json()
|
||||
|
||||
if (!data || data.length === 0) {
|
||||
throw new Error('No workspace members found in Attio response')
|
||||
}
|
||||
|
||||
const member = data[0]
|
||||
|
||||
return {
|
||||
id: `${member.id.workspace_member_id}-${crypto.randomUUID()}`,
|
||||
email: member.email_address,
|
||||
name:
|
||||
`${member.first_name ?? ''} ${member.last_name ?? ''}`.trim() ||
|
||||
member.email_address,
|
||||
emailVerified: true,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
image: member.avatar_url || undefined,
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error('Error in Attio getUserInfo:', error)
|
||||
throw error
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
providerId: 'dropbox',
|
||||
clientId: env.DROPBOX_CLIENT_ID as string,
|
||||
|
||||
@@ -2,7 +2,11 @@ import crypto from 'crypto'
|
||||
import { createLogger } from '@sim/logger'
|
||||
import type { PermissionGroupConfig } from '@/lib/permission-groups/types'
|
||||
import { getEffectiveBlockOutputs } from '@/lib/workflows/blocks/block-outputs'
|
||||
import { buildCanonicalIndex, isCanonicalPair } from '@/lib/workflows/subblocks/visibility'
|
||||
import {
|
||||
buildCanonicalIndex,
|
||||
buildDefaultCanonicalModes,
|
||||
isCanonicalPair,
|
||||
} from '@/lib/workflows/subblocks/visibility'
|
||||
import { hasTriggerCapability } from '@/lib/workflows/triggers/trigger-utils'
|
||||
import { getAllBlocks } from '@/blocks/registry'
|
||||
import type { BlockConfig } from '@/blocks/types'
|
||||
@@ -130,6 +134,12 @@ export function createBlockFromParams(
|
||||
}
|
||||
})
|
||||
|
||||
const defaultModes = buildDefaultCanonicalModes(blockConfig.subBlocks)
|
||||
if (Object.keys(defaultModes).length > 0) {
|
||||
if (!blockState.data) blockState.data = {}
|
||||
blockState.data.canonicalModes = defaultModes
|
||||
}
|
||||
|
||||
if (validatedInputs) {
|
||||
updateCanonicalModesForInputs(blockState, Object.keys(validatedInputs), blockConfig)
|
||||
}
|
||||
|
||||
@@ -281,6 +281,8 @@ export const env = createEnv({
|
||||
SPOTIFY_CLIENT_ID: z.string().optional(), // Spotify OAuth client ID
|
||||
SPOTIFY_CLIENT_SECRET: z.string().optional(), // Spotify OAuth client secret
|
||||
CALCOM_CLIENT_ID: z.string().optional(), // Cal.com OAuth client ID
|
||||
ATTIO_CLIENT_ID: z.string().optional(), // Attio OAuth client ID
|
||||
ATTIO_CLIENT_SECRET: z.string().optional(), // Attio OAuth client secret
|
||||
|
||||
// E2B Remote Code Execution
|
||||
E2B_ENABLED: z.string().optional(), // Enable E2B remote code execution
|
||||
|
||||
@@ -19,8 +19,8 @@ describe('call-chain', () => {
|
||||
})
|
||||
|
||||
describe('MAX_CALL_CHAIN_DEPTH', () => {
|
||||
it('equals 10', () => {
|
||||
expect(MAX_CALL_CHAIN_DEPTH).toBe(10)
|
||||
it('equals 25', () => {
|
||||
expect(MAX_CALL_CHAIN_DEPTH).toBe(25)
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
*/
|
||||
|
||||
export const SIM_VIA_HEADER = 'X-Sim-Via'
|
||||
export const MAX_CALL_CHAIN_DEPTH = 10
|
||||
export const MAX_CALL_CHAIN_DEPTH = 25
|
||||
|
||||
/**
|
||||
* Parses the `X-Sim-Via` header value into an ordered list of workflow IDs.
|
||||
|
||||
@@ -170,7 +170,8 @@ class McpService {
|
||||
userId: string,
|
||||
serverId: string,
|
||||
toolCall: McpToolCall,
|
||||
workspaceId: string
|
||||
workspaceId: string,
|
||||
extraHeaders?: Record<string, string>
|
||||
): Promise<McpToolResult> {
|
||||
const requestId = generateRequestId()
|
||||
const maxRetries = 2
|
||||
@@ -187,6 +188,9 @@ class McpService {
|
||||
}
|
||||
|
||||
const resolvedConfig = await this.resolveConfigEnvVars(config, userId, workspaceId)
|
||||
if (extraHeaders && Object.keys(extraHeaders).length > 0) {
|
||||
resolvedConfig.headers = { ...resolvedConfig.headers, ...extraHeaders }
|
||||
}
|
||||
const client = await this.createClient(resolvedConfig)
|
||||
|
||||
try {
|
||||
|
||||
@@ -2,6 +2,7 @@ import { createLogger } from '@sim/logger'
|
||||
import {
|
||||
AirtableIcon,
|
||||
AsanaIcon,
|
||||
AttioIcon,
|
||||
CalComIcon,
|
||||
ConfluenceIcon,
|
||||
DropboxIcon,
|
||||
@@ -629,6 +630,31 @@ export const OAUTH_PROVIDERS: Record<string, OAuthProviderConfig> = {
|
||||
},
|
||||
defaultService: 'asana',
|
||||
},
|
||||
attio: {
|
||||
name: 'Attio',
|
||||
icon: AttioIcon,
|
||||
services: {
|
||||
attio: {
|
||||
name: 'Attio',
|
||||
description: 'Manage records, notes, tasks, lists, comments, and more in Attio CRM.',
|
||||
providerId: 'attio',
|
||||
icon: AttioIcon,
|
||||
baseProviderIcon: AttioIcon,
|
||||
scopes: [
|
||||
'record_permission:read-write',
|
||||
'object_configuration:read-write',
|
||||
'list_configuration:read-write',
|
||||
'list_entry:read-write',
|
||||
'note:read-write',
|
||||
'task:read-write',
|
||||
'comment:read-write',
|
||||
'user_management:read',
|
||||
'webhook:read-write',
|
||||
],
|
||||
},
|
||||
},
|
||||
defaultService: 'attio',
|
||||
},
|
||||
calcom: {
|
||||
name: 'Cal.com',
|
||||
icon: CalComIcon,
|
||||
@@ -966,6 +992,18 @@ function getProviderAuthConfig(provider: string): ProviderAuthConfig {
|
||||
supportsRefreshTokenRotation: true,
|
||||
}
|
||||
}
|
||||
case 'attio': {
|
||||
const { clientId, clientSecret } = getCredentials(
|
||||
env.ATTIO_CLIENT_ID,
|
||||
env.ATTIO_CLIENT_SECRET
|
||||
)
|
||||
return {
|
||||
tokenEndpoint: 'https://app.attio.com/oauth/token',
|
||||
clientId,
|
||||
clientSecret,
|
||||
useBasicAuth: false,
|
||||
}
|
||||
}
|
||||
case 'dropbox': {
|
||||
const { clientId, clientSecret } = getCredentials(
|
||||
env.DROPBOX_CLIENT_ID,
|
||||
|
||||
@@ -34,6 +34,7 @@ export type OAuthProvider =
|
||||
| 'wealthbox'
|
||||
| 'webflow'
|
||||
| 'asana'
|
||||
| 'attio'
|
||||
| 'pipedrive'
|
||||
| 'hubspot'
|
||||
| 'salesforce'
|
||||
@@ -76,6 +77,7 @@ export type OAuthService =
|
||||
| 'webflow'
|
||||
| 'trello'
|
||||
| 'asana'
|
||||
| 'attio'
|
||||
| 'pipedrive'
|
||||
| 'hubspot'
|
||||
| 'salesforce'
|
||||
|
||||
@@ -13,6 +13,7 @@ import { convertSquareBracketsToTwiML } from '@/lib/webhooks/utils'
|
||||
import {
|
||||
handleSlackChallenge,
|
||||
handleWhatsAppVerification,
|
||||
validateAttioSignature,
|
||||
validateCalcomSignature,
|
||||
validateCirclebackSignature,
|
||||
validateFirefliesSignature,
|
||||
@@ -597,6 +598,33 @@ export async function verifyProviderAuth(
|
||||
}
|
||||
}
|
||||
|
||||
if (foundWebhook.provider === 'attio') {
|
||||
const secret = providerConfig.webhookSecret as string | undefined
|
||||
|
||||
if (!secret) {
|
||||
logger.debug(
|
||||
`[${requestId}] Attio webhook ${foundWebhook.id} has no signing secret, skipping signature verification`
|
||||
)
|
||||
} else {
|
||||
const signature = request.headers.get('Attio-Signature')
|
||||
|
||||
if (!signature) {
|
||||
logger.warn(`[${requestId}] Attio webhook missing signature header`)
|
||||
return new NextResponse('Unauthorized - Missing Attio signature', { status: 401 })
|
||||
}
|
||||
|
||||
const isValidSignature = validateAttioSignature(secret, signature, rawBody)
|
||||
|
||||
if (!isValidSignature) {
|
||||
logger.warn(`[${requestId}] Attio signature verification failed`, {
|
||||
signatureLength: signature.length,
|
||||
secretLength: secret.length,
|
||||
})
|
||||
return new NextResponse('Unauthorized - Invalid Attio signature', { status: 401 })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (foundWebhook.provider === 'linear') {
|
||||
const secret = providerConfig.webhookSecret as string | undefined
|
||||
|
||||
@@ -946,6 +974,30 @@ export async function queueWebhookExecution(
|
||||
}
|
||||
}
|
||||
|
||||
if (foundWebhook.provider === 'attio') {
|
||||
const providerConfig = (foundWebhook.providerConfig as Record<string, any>) || {}
|
||||
const triggerId = providerConfig.triggerId as string | undefined
|
||||
|
||||
if (triggerId && triggerId !== 'attio_webhook') {
|
||||
const { isAttioPayloadMatch, getAttioEvent } = await import('@/triggers/attio/utils')
|
||||
if (!isAttioPayloadMatch(triggerId, body)) {
|
||||
const event = getAttioEvent(body)
|
||||
const eventType = event?.event_type as string | undefined
|
||||
logger.debug(
|
||||
`[${options.requestId}] Attio event mismatch for trigger ${triggerId}. Event: ${eventType}. Skipping execution.`,
|
||||
{
|
||||
webhookId: foundWebhook.id,
|
||||
workflowId: foundWorkflow.id,
|
||||
triggerId,
|
||||
receivedEvent: eventType,
|
||||
bodyKeys: Object.keys(body),
|
||||
}
|
||||
)
|
||||
return NextResponse.json({ status: 'skipped', reason: 'event_type_mismatch' })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (foundWebhook.provider === 'hubspot') {
|
||||
const providerConfig = (foundWebhook.providerConfig as Record<string, any>) || {}
|
||||
const triggerId = providerConfig.triggerId as string | undefined
|
||||
|
||||
@@ -19,6 +19,7 @@ const calendlyLogger = createLogger('CalendlyWebhook')
|
||||
const grainLogger = createLogger('GrainWebhook')
|
||||
const lemlistLogger = createLogger('LemlistWebhook')
|
||||
const webflowLogger = createLogger('WebflowWebhook')
|
||||
const attioLogger = createLogger('AttioWebhook')
|
||||
const providerSubscriptionsLogger = createLogger('WebhookProviderSubscriptions')
|
||||
|
||||
function getProviderConfig(webhook: any): Record<string, any> {
|
||||
@@ -976,6 +977,203 @@ export async function deleteWebflowWebhook(
|
||||
}
|
||||
}
|
||||
|
||||
export async function createAttioWebhookSubscription(
|
||||
userId: string,
|
||||
webhookData: any,
|
||||
requestId: string
|
||||
): Promise<{ externalId: string; webhookSecret: string } | undefined> {
|
||||
try {
|
||||
const { path, providerConfig } = webhookData
|
||||
const { triggerId, credentialId } = providerConfig || {}
|
||||
|
||||
if (!credentialId) {
|
||||
attioLogger.warn(`[${requestId}] Missing credentialId for Attio webhook creation.`, {
|
||||
webhookId: webhookData.id,
|
||||
})
|
||||
throw new Error(
|
||||
'Attio account connection required. Please connect your Attio account in the trigger configuration and try again.'
|
||||
)
|
||||
}
|
||||
|
||||
const credentialOwner = await getCredentialOwner(credentialId, requestId)
|
||||
const accessToken = credentialOwner
|
||||
? await refreshAccessTokenIfNeeded(
|
||||
credentialOwner.accountId,
|
||||
credentialOwner.userId,
|
||||
requestId
|
||||
)
|
||||
: null
|
||||
|
||||
if (!accessToken) {
|
||||
attioLogger.warn(
|
||||
`[${requestId}] Could not retrieve Attio access token for user ${userId}. Cannot create webhook.`
|
||||
)
|
||||
throw new Error(
|
||||
'Attio account connection required. Please connect your Attio account in the trigger configuration and try again.'
|
||||
)
|
||||
}
|
||||
|
||||
const notificationUrl = `${getBaseUrl()}/api/webhooks/trigger/${path}`
|
||||
|
||||
const { TRIGGER_EVENT_MAP } = await import('@/triggers/attio/utils')
|
||||
|
||||
let subscriptions: Array<{ event_type: string; filter: null }> = []
|
||||
if (triggerId === 'attio_webhook') {
|
||||
const allEvents = new Set<string>()
|
||||
for (const events of Object.values(TRIGGER_EVENT_MAP)) {
|
||||
for (const event of events) {
|
||||
allEvents.add(event)
|
||||
}
|
||||
}
|
||||
subscriptions = Array.from(allEvents).map((event_type) => ({ event_type, filter: null }))
|
||||
} else {
|
||||
const events = TRIGGER_EVENT_MAP[triggerId]
|
||||
if (!events || events.length === 0) {
|
||||
attioLogger.warn(`[${requestId}] No event types mapped for trigger ${triggerId}`, {
|
||||
webhookId: webhookData.id,
|
||||
})
|
||||
throw new Error(`Unknown Attio trigger type: ${triggerId}`)
|
||||
}
|
||||
subscriptions = events.map((event_type) => ({ event_type, filter: null }))
|
||||
}
|
||||
|
||||
const requestBody = {
|
||||
data: {
|
||||
target_url: notificationUrl,
|
||||
subscriptions,
|
||||
},
|
||||
}
|
||||
|
||||
const attioResponse = await fetch('https://api.attio.com/v2/webhooks', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(requestBody),
|
||||
})
|
||||
|
||||
if (!attioResponse.ok) {
|
||||
const errorBody = await attioResponse.json().catch(() => ({}))
|
||||
attioLogger.error(
|
||||
`[${requestId}] Failed to create webhook in Attio for webhook ${webhookData.id}. Status: ${attioResponse.status}`,
|
||||
{ response: errorBody }
|
||||
)
|
||||
|
||||
let userFriendlyMessage = 'Failed to create webhook subscription in Attio'
|
||||
if (attioResponse.status === 401) {
|
||||
userFriendlyMessage = 'Attio authentication failed. Please reconnect your Attio account.'
|
||||
} else if (attioResponse.status === 403) {
|
||||
userFriendlyMessage =
|
||||
'Attio access denied. Please ensure your integration has webhook permissions.'
|
||||
}
|
||||
|
||||
throw new Error(userFriendlyMessage)
|
||||
}
|
||||
|
||||
const responseBody = await attioResponse.json()
|
||||
const data = responseBody.data || responseBody
|
||||
const webhookId = data.id?.webhook_id || data.webhook_id || data.id
|
||||
const secret = data.secret
|
||||
|
||||
if (!webhookId) {
|
||||
attioLogger.error(
|
||||
`[${requestId}] Attio webhook created but no webhook_id returned for webhook ${webhookData.id}`,
|
||||
{ response: responseBody }
|
||||
)
|
||||
throw new Error('Attio webhook creation succeeded but no webhook ID was returned')
|
||||
}
|
||||
|
||||
if (!secret) {
|
||||
attioLogger.warn(
|
||||
`[${requestId}] Attio webhook created but no secret returned for webhook ${webhookData.id}. Signature verification will be skipped.`,
|
||||
{ response: responseBody }
|
||||
)
|
||||
}
|
||||
|
||||
attioLogger.info(
|
||||
`[${requestId}] Successfully created webhook in Attio for webhook ${webhookData.id}.`,
|
||||
{
|
||||
attioWebhookId: webhookId,
|
||||
targetUrl: notificationUrl,
|
||||
subscriptionCount: subscriptions.length,
|
||||
status: data.status,
|
||||
}
|
||||
)
|
||||
|
||||
return { externalId: webhookId, webhookSecret: secret || '' }
|
||||
} catch (error: unknown) {
|
||||
const message = error instanceof Error ? error.message : String(error)
|
||||
attioLogger.error(
|
||||
`[${requestId}] Exception during Attio webhook creation for webhook ${webhookData.id}.`,
|
||||
{ message }
|
||||
)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
export async function deleteAttioWebhook(
|
||||
webhook: any,
|
||||
_workflow: any,
|
||||
requestId: string
|
||||
): Promise<void> {
|
||||
try {
|
||||
const config = getProviderConfig(webhook)
|
||||
const externalId = config.externalId as string | undefined
|
||||
const credentialId = config.credentialId as string | undefined
|
||||
|
||||
if (!externalId) {
|
||||
attioLogger.warn(
|
||||
`[${requestId}] Missing externalId for Attio webhook deletion ${webhook.id}, skipping cleanup`
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
if (!credentialId) {
|
||||
attioLogger.warn(
|
||||
`[${requestId}] Missing credentialId for Attio webhook deletion ${webhook.id}, skipping cleanup`
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
const credentialOwner = await getCredentialOwner(credentialId, requestId)
|
||||
const accessToken = credentialOwner
|
||||
? await refreshAccessTokenIfNeeded(
|
||||
credentialOwner.accountId,
|
||||
credentialOwner.userId,
|
||||
requestId
|
||||
)
|
||||
: null
|
||||
|
||||
if (!accessToken) {
|
||||
attioLogger.warn(
|
||||
`[${requestId}] Could not retrieve Attio access token. Cannot delete webhook.`,
|
||||
{ webhookId: webhook.id }
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
const attioResponse = await fetch(`https://api.attio.com/v2/webhooks/${externalId}`, {
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
},
|
||||
})
|
||||
|
||||
if (!attioResponse.ok && attioResponse.status !== 404) {
|
||||
const responseBody = await attioResponse.json().catch(() => ({}))
|
||||
attioLogger.warn(
|
||||
`[${requestId}] Failed to delete Attio webhook (non-fatal): ${attioResponse.status}`,
|
||||
{ response: responseBody }
|
||||
)
|
||||
} else {
|
||||
attioLogger.info(`[${requestId}] Successfully deleted Attio webhook ${externalId}`)
|
||||
}
|
||||
} catch (error) {
|
||||
attioLogger.warn(`[${requestId}] Error deleting Attio webhook (non-fatal)`, error)
|
||||
}
|
||||
}
|
||||
|
||||
export async function createGrainWebhookSubscription(
|
||||
_request: NextRequest,
|
||||
webhookData: any,
|
||||
@@ -1611,6 +1809,7 @@ type RecreateCheckInput = {
|
||||
/** Providers that create external webhook subscriptions */
|
||||
const PROVIDERS_WITH_EXTERNAL_SUBSCRIPTIONS = new Set([
|
||||
'airtable',
|
||||
'attio',
|
||||
'calendly',
|
||||
'webflow',
|
||||
'typeform',
|
||||
@@ -1626,6 +1825,7 @@ const SYSTEM_MANAGED_FIELDS = new Set([
|
||||
'externalSubscriptionId',
|
||||
'eventTypes',
|
||||
'webhookTag',
|
||||
'webhookSecret',
|
||||
'historyId',
|
||||
'lastCheckedTimestamp',
|
||||
'setupCompleted',
|
||||
@@ -1686,6 +1886,16 @@ export async function createExternalWebhookSubscription(
|
||||
updatedProviderConfig = { ...updatedProviderConfig, externalId }
|
||||
externalSubscriptionCreated = true
|
||||
}
|
||||
} else if (provider === 'attio') {
|
||||
const result = await createAttioWebhookSubscription(userId, webhookData, requestId)
|
||||
if (result) {
|
||||
updatedProviderConfig = {
|
||||
...updatedProviderConfig,
|
||||
externalId: result.externalId,
|
||||
webhookSecret: result.webhookSecret,
|
||||
}
|
||||
externalSubscriptionCreated = true
|
||||
}
|
||||
} else if (provider === 'calendly') {
|
||||
const externalId = await createCalendlyWebhookSubscription(webhookData, requestId)
|
||||
if (externalId) {
|
||||
@@ -1736,7 +1946,7 @@ export async function createExternalWebhookSubscription(
|
||||
|
||||
/**
|
||||
* Clean up external webhook subscriptions for a webhook
|
||||
* Handles Airtable, Teams, Telegram, Typeform, Calendly, Grain, and Lemlist cleanup
|
||||
* Handles Airtable, Attio, Teams, Telegram, Typeform, Calendly, Grain, and Lemlist cleanup
|
||||
* Don't fail deletion if cleanup fails
|
||||
*/
|
||||
export async function cleanupExternalWebhook(
|
||||
@@ -1746,6 +1956,8 @@ export async function cleanupExternalWebhook(
|
||||
): Promise<void> {
|
||||
if (webhook.provider === 'airtable') {
|
||||
await deleteAirtableWebhook(webhook, workflow, requestId)
|
||||
} else if (webhook.provider === 'attio') {
|
||||
await deleteAttioWebhook(webhook, workflow, requestId)
|
||||
} else if (webhook.provider === 'microsoft-teams') {
|
||||
await deleteTeamsSubscription(webhook, workflow, requestId)
|
||||
} else if (webhook.provider === 'telegram') {
|
||||
|
||||
@@ -1291,6 +1291,49 @@ export async function formatWebhookInput(
|
||||
}
|
||||
}
|
||||
|
||||
if (foundWebhook.provider === 'attio') {
|
||||
const {
|
||||
extractAttioRecordData,
|
||||
extractAttioRecordUpdatedData,
|
||||
extractAttioRecordMergedData,
|
||||
extractAttioNoteData,
|
||||
extractAttioTaskData,
|
||||
extractAttioCommentData,
|
||||
extractAttioListEntryData,
|
||||
extractAttioListEntryUpdatedData,
|
||||
extractAttioGenericData,
|
||||
} = await import('@/triggers/attio/utils')
|
||||
|
||||
const providerConfig = (foundWebhook.providerConfig as Record<string, any>) || {}
|
||||
const triggerId = providerConfig.triggerId as string | undefined
|
||||
|
||||
if (triggerId === 'attio_record_updated') {
|
||||
return extractAttioRecordUpdatedData(body)
|
||||
}
|
||||
if (triggerId === 'attio_record_merged') {
|
||||
return extractAttioRecordMergedData(body)
|
||||
}
|
||||
if (triggerId === 'attio_record_created' || triggerId === 'attio_record_deleted') {
|
||||
return extractAttioRecordData(body)
|
||||
}
|
||||
if (triggerId?.startsWith('attio_note_')) {
|
||||
return extractAttioNoteData(body)
|
||||
}
|
||||
if (triggerId?.startsWith('attio_task_')) {
|
||||
return extractAttioTaskData(body)
|
||||
}
|
||||
if (triggerId?.startsWith('attio_comment_')) {
|
||||
return extractAttioCommentData(body)
|
||||
}
|
||||
if (triggerId === 'attio_list_entry_updated') {
|
||||
return extractAttioListEntryUpdatedData(body)
|
||||
}
|
||||
if (triggerId === 'attio_list_entry_created' || triggerId === 'attio_list_entry_deleted') {
|
||||
return extractAttioListEntryData(body)
|
||||
}
|
||||
return extractAttioGenericData(body)
|
||||
}
|
||||
|
||||
return body
|
||||
}
|
||||
|
||||
@@ -1395,6 +1438,41 @@ export function validateLinearSignature(secret: string, signature: string, body:
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates an Attio webhook request signature using HMAC SHA-256
|
||||
* @param secret - Attio webhook signing secret (plain text)
|
||||
* @param signature - Attio-Signature header value (hex-encoded HMAC SHA-256 signature)
|
||||
* @param body - Raw request body string
|
||||
* @returns Whether the signature is valid
|
||||
*/
|
||||
export function validateAttioSignature(secret: string, signature: string, body: string): boolean {
|
||||
try {
|
||||
if (!secret || !signature || !body) {
|
||||
logger.warn('Attio signature validation missing required fields', {
|
||||
hasSecret: !!secret,
|
||||
hasSignature: !!signature,
|
||||
hasBody: !!body,
|
||||
})
|
||||
return false
|
||||
}
|
||||
|
||||
const computedHash = crypto.createHmac('sha256', secret).update(body, 'utf8').digest('hex')
|
||||
|
||||
logger.debug('Attio signature comparison', {
|
||||
computedSignature: `${computedHash.substring(0, 10)}...`,
|
||||
providedSignature: `${signature.substring(0, 10)}...`,
|
||||
computedLength: computedHash.length,
|
||||
providedLength: signature.length,
|
||||
match: computedHash === signature,
|
||||
})
|
||||
|
||||
return safeCompare(computedHash, signature)
|
||||
} catch (error) {
|
||||
logger.error('Error validating Attio signature:', error)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates a Circleback webhook request signature using HMAC SHA-256
|
||||
* @param secret - Circleback signing secret (plain text)
|
||||
|
||||
@@ -75,6 +75,23 @@ export function isCanonicalPair(group?: CanonicalGroup): boolean {
|
||||
return Boolean(group?.basicId && group?.advancedIds?.length)
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds default canonical mode overrides for a block's subblocks.
|
||||
* All canonical pairs default to `'basic'`.
|
||||
*/
|
||||
export function buildDefaultCanonicalModes(
|
||||
subBlocks: SubBlockConfig[]
|
||||
): Record<string, 'basic' | 'advanced'> {
|
||||
const index = buildCanonicalIndex(subBlocks)
|
||||
const modes: Record<string, 'basic' | 'advanced'> = {}
|
||||
for (const group of Object.values(index.groupsById)) {
|
||||
if (isCanonicalPair(group)) {
|
||||
modes[group.canonicalId] = 'basic'
|
||||
}
|
||||
}
|
||||
return modes
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine the active mode for a canonical group.
|
||||
*/
|
||||
|
||||
@@ -138,14 +138,20 @@ const ANTHROPIC_SDK_NON_STREAMING_MAX_TOKENS = 21333
|
||||
*/
|
||||
async function createMessage(
|
||||
anthropic: Anthropic,
|
||||
payload: AnthropicPayload
|
||||
payload: AnthropicPayload,
|
||||
abortSignal?: AbortSignal
|
||||
): Promise<Anthropic.Messages.Message> {
|
||||
const options = abortSignal ? { signal: abortSignal } : undefined
|
||||
if (payload.max_tokens > ANTHROPIC_SDK_NON_STREAMING_MAX_TOKENS && !payload.stream) {
|
||||
const stream = anthropic.messages.stream(payload as Anthropic.Messages.MessageStreamParams)
|
||||
const stream = anthropic.messages.stream(
|
||||
payload as Anthropic.Messages.MessageStreamParams,
|
||||
options
|
||||
)
|
||||
return stream.finalMessage()
|
||||
}
|
||||
return anthropic.messages.create(
|
||||
payload as Anthropic.Messages.MessageCreateParamsNonStreaming
|
||||
payload as Anthropic.Messages.MessageCreateParamsNonStreaming,
|
||||
options
|
||||
) as Promise<Anthropic.Messages.Message>
|
||||
}
|
||||
|
||||
@@ -367,10 +373,13 @@ export async function executeAnthropicProviderRequest(
|
||||
const providerStartTime = Date.now()
|
||||
const providerStartTimeISO = new Date(providerStartTime).toISOString()
|
||||
|
||||
const streamResponse = await anthropic.messages.create({
|
||||
...payload,
|
||||
stream: true,
|
||||
} as Anthropic.Messages.MessageCreateParamsStreaming)
|
||||
const streamResponse = await anthropic.messages.create(
|
||||
{
|
||||
...payload,
|
||||
stream: true,
|
||||
} as Anthropic.Messages.MessageCreateParamsStreaming,
|
||||
request.abortSignal ? { signal: request.abortSignal } : undefined
|
||||
)
|
||||
|
||||
const streamingResult = {
|
||||
stream: createReadableStreamFromAnthropicStream(
|
||||
@@ -461,7 +470,7 @@ export async function executeAnthropicProviderRequest(
|
||||
const forcedTools = preparedTools?.forcedTools || []
|
||||
let usedForcedTools: string[] = []
|
||||
|
||||
let currentResponse = await createMessage(anthropic, payload)
|
||||
let currentResponse = await createMessage(anthropic, payload, request.abortSignal)
|
||||
const firstResponseTime = Date.now() - initialCallTime
|
||||
|
||||
let content = ''
|
||||
@@ -708,7 +717,7 @@ export async function executeAnthropicProviderRequest(
|
||||
|
||||
const nextModelStartTime = Date.now()
|
||||
|
||||
currentResponse = await createMessage(anthropic, nextPayload)
|
||||
currentResponse = await createMessage(anthropic, nextPayload, request.abortSignal)
|
||||
|
||||
const nextCheckResult = checkForForcedToolUsage(
|
||||
currentResponse,
|
||||
@@ -758,7 +767,8 @@ export async function executeAnthropicProviderRequest(
|
||||
}
|
||||
|
||||
const streamResponse = await anthropic.messages.create(
|
||||
streamingPayload as Anthropic.Messages.MessageCreateParamsStreaming
|
||||
streamingPayload as Anthropic.Messages.MessageCreateParamsStreaming,
|
||||
request.abortSignal ? { signal: request.abortSignal } : undefined
|
||||
)
|
||||
|
||||
const streamingResult = {
|
||||
@@ -860,7 +870,7 @@ export async function executeAnthropicProviderRequest(
|
||||
const forcedTools = preparedTools?.forcedTools || []
|
||||
let usedForcedTools: string[] = []
|
||||
|
||||
let currentResponse = await createMessage(anthropic, payload)
|
||||
let currentResponse = await createMessage(anthropic, payload, request.abortSignal)
|
||||
const firstResponseTime = Date.now() - initialCallTime
|
||||
|
||||
let content = ''
|
||||
@@ -1118,7 +1128,7 @@ export async function executeAnthropicProviderRequest(
|
||||
|
||||
const nextModelStartTime = Date.now()
|
||||
|
||||
currentResponse = await createMessage(anthropic, nextPayload)
|
||||
currentResponse = await createMessage(anthropic, nextPayload, request.abortSignal)
|
||||
|
||||
const nextCheckResult = checkForForcedToolUsage(
|
||||
currentResponse,
|
||||
@@ -1182,7 +1192,8 @@ export async function executeAnthropicProviderRequest(
|
||||
}
|
||||
|
||||
const streamResponse = await anthropic.messages.create(
|
||||
streamingPayload as Anthropic.Messages.MessageCreateParamsStreaming
|
||||
streamingPayload as Anthropic.Messages.MessageCreateParamsStreaming,
|
||||
request.abortSignal ? { signal: request.abortSignal } : undefined
|
||||
)
|
||||
|
||||
const streamingResult = {
|
||||
|
||||
@@ -165,7 +165,10 @@ async function executeChatCompletionsRequest(
|
||||
stream: true,
|
||||
stream_options: { include_usage: true },
|
||||
}
|
||||
const streamResponse = await azureOpenAI.chat.completions.create(streamingParams)
|
||||
const streamResponse = await azureOpenAI.chat.completions.create(
|
||||
streamingParams,
|
||||
request.abortSignal ? { signal: request.abortSignal } : undefined
|
||||
)
|
||||
|
||||
const streamingResult = {
|
||||
stream: createReadableStreamFromAzureOpenAIStream(streamResponse, (content, usage) => {
|
||||
@@ -243,7 +246,10 @@ async function executeChatCompletionsRequest(
|
||||
const forcedTools = preparedTools?.forcedTools || []
|
||||
let usedForcedTools: string[] = []
|
||||
|
||||
let currentResponse = (await azureOpenAI.chat.completions.create(payload)) as ChatCompletion
|
||||
let currentResponse = (await azureOpenAI.chat.completions.create(
|
||||
payload,
|
||||
request.abortSignal ? { signal: request.abortSignal } : undefined
|
||||
)) as ChatCompletion
|
||||
const firstResponseTime = Date.now() - initialCallTime
|
||||
|
||||
let content = currentResponse.choices[0]?.message?.content || ''
|
||||
@@ -421,7 +427,10 @@ async function executeChatCompletionsRequest(
|
||||
}
|
||||
|
||||
const nextModelStartTime = Date.now()
|
||||
currentResponse = (await azureOpenAI.chat.completions.create(nextPayload)) as ChatCompletion
|
||||
currentResponse = (await azureOpenAI.chat.completions.create(
|
||||
nextPayload,
|
||||
request.abortSignal ? { signal: request.abortSignal } : undefined
|
||||
)) as ChatCompletion
|
||||
|
||||
const nextCheckResult = checkForForcedToolUsage(
|
||||
currentResponse,
|
||||
@@ -471,7 +480,10 @@ async function executeChatCompletionsRequest(
|
||||
stream: true,
|
||||
stream_options: { include_usage: true },
|
||||
}
|
||||
const streamResponse = await azureOpenAI.chat.completions.create(streamingParams)
|
||||
const streamResponse = await azureOpenAI.chat.completions.create(
|
||||
streamingParams,
|
||||
request.abortSignal ? { signal: request.abortSignal } : undefined
|
||||
)
|
||||
|
||||
const streamingResult = {
|
||||
stream: createReadableStreamFromAzureOpenAIStream(streamResponse, (content, usage) => {
|
||||
|
||||
@@ -284,7 +284,10 @@ export const bedrockProvider: ProviderConfig = {
|
||||
inferenceConfig,
|
||||
})
|
||||
|
||||
const streamResponse = await client.send(command)
|
||||
const streamResponse = await client.send(
|
||||
command,
|
||||
request.abortSignal ? { abortSignal: request.abortSignal } : undefined
|
||||
)
|
||||
|
||||
if (!streamResponse.stream) {
|
||||
throw new Error('No stream returned from Bedrock')
|
||||
@@ -379,7 +382,10 @@ export const bedrockProvider: ProviderConfig = {
|
||||
toolConfig,
|
||||
})
|
||||
|
||||
let currentResponse = await client.send(command)
|
||||
let currentResponse = await client.send(
|
||||
command,
|
||||
request.abortSignal ? { abortSignal: request.abortSignal } : undefined
|
||||
)
|
||||
const firstResponseTime = Date.now() - initialCallTime
|
||||
|
||||
let content = ''
|
||||
@@ -628,7 +634,10 @@ export const bedrockProvider: ProviderConfig = {
|
||||
: undefined,
|
||||
})
|
||||
|
||||
currentResponse = await client.send(nextCommand)
|
||||
currentResponse = await client.send(
|
||||
nextCommand,
|
||||
request.abortSignal ? { abortSignal: request.abortSignal } : undefined
|
||||
)
|
||||
|
||||
const nextToolUseContentBlocks = (currentResponse.output?.message?.content || []).filter(
|
||||
(block): block is ContentBlock & { toolUse: ToolUseBlock } => 'toolUse' in block
|
||||
@@ -696,7 +705,10 @@ export const bedrockProvider: ProviderConfig = {
|
||||
},
|
||||
})
|
||||
|
||||
const structuredResponse = await client.send(structuredOutputCommand)
|
||||
const structuredResponse = await client.send(
|
||||
structuredOutputCommand,
|
||||
request.abortSignal ? { abortSignal: request.abortSignal } : undefined
|
||||
)
|
||||
const structuredOutputEndTime = Date.now()
|
||||
|
||||
timeSegments.push({
|
||||
@@ -782,7 +794,10 @@ export const bedrockProvider: ProviderConfig = {
|
||||
toolConfig: streamToolConfig,
|
||||
})
|
||||
|
||||
const streamResponse = await client.send(streamCommand)
|
||||
const streamResponse = await client.send(
|
||||
streamCommand,
|
||||
request.abortSignal ? { abortSignal: request.abortSignal } : undefined
|
||||
)
|
||||
|
||||
if (!streamResponse.stream) {
|
||||
throw new Error('No stream returned from Bedrock')
|
||||
|
||||
@@ -117,10 +117,13 @@ export const cerebrasProvider: ProviderConfig = {
|
||||
if (request.stream && (!tools || tools.length === 0)) {
|
||||
logger.info('Using streaming response for Cerebras request (no tools)')
|
||||
|
||||
const streamResponse: any = await client.chat.completions.create({
|
||||
...payload,
|
||||
stream: true,
|
||||
})
|
||||
const streamResponse: any = await client.chat.completions.create(
|
||||
{
|
||||
...payload,
|
||||
stream: true,
|
||||
},
|
||||
request.abortSignal ? { signal: request.abortSignal } : undefined
|
||||
)
|
||||
|
||||
const streamingResult = {
|
||||
stream: createReadableStreamFromCerebrasStream(streamResponse, (content, usage) => {
|
||||
@@ -179,7 +182,10 @@ export const cerebrasProvider: ProviderConfig = {
|
||||
}
|
||||
const initialCallTime = Date.now()
|
||||
|
||||
let currentResponse = (await client.chat.completions.create(payload)) as CerebrasResponse
|
||||
let currentResponse = (await client.chat.completions.create(
|
||||
payload,
|
||||
request.abortSignal ? { signal: request.abortSignal } : undefined
|
||||
)) as CerebrasResponse
|
||||
const firstResponseTime = Date.now() - initialCallTime
|
||||
|
||||
let content = currentResponse.choices[0]?.message?.content || ''
|
||||
@@ -365,7 +371,8 @@ export const cerebrasProvider: ProviderConfig = {
|
||||
finalPayload.tool_choice = 'none'
|
||||
|
||||
const finalResponse = (await client.chat.completions.create(
|
||||
finalPayload
|
||||
finalPayload,
|
||||
request.abortSignal ? { signal: request.abortSignal } : undefined
|
||||
)) as CerebrasResponse
|
||||
|
||||
const nextModelEndTime = Date.now()
|
||||
@@ -401,7 +408,8 @@ export const cerebrasProvider: ProviderConfig = {
|
||||
|
||||
const nextModelStartTime = Date.now()
|
||||
currentResponse = (await client.chat.completions.create(
|
||||
nextPayload
|
||||
nextPayload,
|
||||
request.abortSignal ? { signal: request.abortSignal } : undefined
|
||||
)) as CerebrasResponse
|
||||
|
||||
const nextModelEndTime = Date.now()
|
||||
@@ -443,7 +451,10 @@ export const cerebrasProvider: ProviderConfig = {
|
||||
stream: true,
|
||||
}
|
||||
|
||||
const streamResponse: any = await client.chat.completions.create(streamingPayload)
|
||||
const streamResponse: any = await client.chat.completions.create(
|
||||
streamingPayload,
|
||||
request.abortSignal ? { signal: request.abortSignal } : undefined
|
||||
)
|
||||
|
||||
const accumulatedCost = calculateCost(request.model, tokens.input, tokens.output)
|
||||
|
||||
|
||||
@@ -114,10 +114,13 @@ export const deepseekProvider: ProviderConfig = {
|
||||
if (request.stream && (!tools || tools.length === 0)) {
|
||||
logger.info('Using streaming response for DeepSeek request (no tools)')
|
||||
|
||||
const streamResponse = await deepseek.chat.completions.create({
|
||||
...payload,
|
||||
stream: true,
|
||||
})
|
||||
const streamResponse = await deepseek.chat.completions.create(
|
||||
{
|
||||
...payload,
|
||||
stream: true,
|
||||
},
|
||||
request.abortSignal ? { signal: request.abortSignal } : undefined
|
||||
)
|
||||
|
||||
const streamingResult = {
|
||||
stream: createReadableStreamFromDeepseekStream(
|
||||
@@ -183,7 +186,10 @@ export const deepseekProvider: ProviderConfig = {
|
||||
const forcedTools = preparedTools?.forcedTools || []
|
||||
let usedForcedTools: string[] = []
|
||||
|
||||
let currentResponse = await deepseek.chat.completions.create(payload)
|
||||
let currentResponse = await deepseek.chat.completions.create(
|
||||
payload,
|
||||
request.abortSignal ? { signal: request.abortSignal } : undefined
|
||||
)
|
||||
const firstResponseTime = Date.now() - initialCallTime
|
||||
|
||||
let content = currentResponse.choices[0]?.message?.content || ''
|
||||
@@ -375,7 +381,10 @@ export const deepseekProvider: ProviderConfig = {
|
||||
}
|
||||
|
||||
const nextModelStartTime = Date.now()
|
||||
currentResponse = await deepseek.chat.completions.create(nextPayload)
|
||||
currentResponse = await deepseek.chat.completions.create(
|
||||
nextPayload,
|
||||
request.abortSignal ? { signal: request.abortSignal } : undefined
|
||||
)
|
||||
|
||||
if (
|
||||
typeof nextPayload.tool_choice === 'object' &&
|
||||
@@ -439,7 +448,10 @@ export const deepseekProvider: ProviderConfig = {
|
||||
stream: true,
|
||||
}
|
||||
|
||||
const streamResponse = await deepseek.chat.completions.create(streamingPayload)
|
||||
const streamResponse = await deepseek.chat.completions.create(
|
||||
streamingPayload,
|
||||
request.abortSignal ? { signal: request.abortSignal } : undefined
|
||||
)
|
||||
|
||||
const accumulatedCost = calculateCost(request.model, tokens.input, tokens.output)
|
||||
|
||||
|
||||
@@ -387,10 +387,25 @@ const DEEP_RESEARCH_POLL_INTERVAL_MS = 10_000
|
||||
const DEEP_RESEARCH_MAX_DURATION_MS = 60 * 60 * 1000
|
||||
|
||||
/**
|
||||
* Sleeps for the specified number of milliseconds
|
||||
* Sleeps for the specified number of milliseconds, respecting an optional abort signal.
|
||||
*/
|
||||
function sleep(ms: number): Promise<void> {
|
||||
return new Promise((resolve) => setTimeout(resolve, ms))
|
||||
function sleep(ms: number, signal?: AbortSignal): Promise<void> {
|
||||
if (signal?.aborted) {
|
||||
return Promise.reject(
|
||||
signal.reason ?? new DOMException('The operation was aborted.', 'AbortError')
|
||||
)
|
||||
}
|
||||
return new Promise((resolve, reject) => {
|
||||
const onAbort = () => {
|
||||
clearTimeout(timer)
|
||||
reject(signal!.reason ?? new DOMException('The operation was aborted.', 'AbortError'))
|
||||
}
|
||||
const timer = setTimeout(() => {
|
||||
signal?.removeEventListener('abort', onAbort)
|
||||
resolve()
|
||||
}, ms)
|
||||
signal?.addEventListener('abort', onAbort, { once: true })
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -680,7 +695,10 @@ export async function executeDeepResearchRequest(
|
||||
stream: true,
|
||||
}
|
||||
|
||||
const streamResponse = await ai.interactions.create(streamParams)
|
||||
const streamResponse = await ai.interactions.create(
|
||||
streamParams,
|
||||
request.abortSignal ? { signal: request.abortSignal } : undefined
|
||||
)
|
||||
const firstResponseTime = Date.now() - providerStartTime
|
||||
|
||||
const streamingResult: StreamingExecution = {
|
||||
@@ -765,7 +783,10 @@ export async function executeDeepResearchRequest(
|
||||
stream: false,
|
||||
}
|
||||
|
||||
const interaction = await ai.interactions.create(createParams)
|
||||
const interaction = await ai.interactions.create(
|
||||
createParams,
|
||||
request.abortSignal ? { signal: request.abortSignal } : undefined
|
||||
)
|
||||
const interactionId = interaction.id
|
||||
|
||||
logger.info('Deep research interaction created', { interactionId, status: interaction.status })
|
||||
@@ -793,8 +814,12 @@ export async function executeDeepResearchRequest(
|
||||
elapsedMs: Date.now() - pollStartTime,
|
||||
})
|
||||
|
||||
await sleep(DEEP_RESEARCH_POLL_INTERVAL_MS)
|
||||
result = await ai.interactions.get(interactionId)
|
||||
await sleep(DEEP_RESEARCH_POLL_INTERVAL_MS, request.abortSignal)
|
||||
result = await ai.interactions.get(
|
||||
interactionId,
|
||||
undefined,
|
||||
request.abortSignal ? { signal: request.abortSignal } : undefined
|
||||
)
|
||||
}
|
||||
|
||||
if (result.status !== 'completed') {
|
||||
@@ -882,6 +907,9 @@ export async function executeGeminiRequest(
|
||||
// Build configuration
|
||||
const geminiConfig: GenerateContentConfig = {}
|
||||
|
||||
if (request.abortSignal) {
|
||||
geminiConfig.abortSignal = request.abortSignal
|
||||
}
|
||||
if (request.temperature !== undefined) {
|
||||
geminiConfig.temperature = request.temperature
|
||||
}
|
||||
|
||||
@@ -118,10 +118,13 @@ export const groqProvider: ProviderConfig = {
|
||||
const providerStartTime = Date.now()
|
||||
const providerStartTimeISO = new Date(providerStartTime).toISOString()
|
||||
|
||||
const streamResponse = await groq.chat.completions.create({
|
||||
...payload,
|
||||
stream: true,
|
||||
})
|
||||
const streamResponse = await groq.chat.completions.create(
|
||||
{
|
||||
...payload,
|
||||
stream: true,
|
||||
},
|
||||
request.abortSignal ? { signal: request.abortSignal } : undefined
|
||||
)
|
||||
|
||||
const streamingResult = {
|
||||
stream: createReadableStreamFromGroqStream(streamResponse as any, (content, usage) => {
|
||||
@@ -185,7 +188,10 @@ export const groqProvider: ProviderConfig = {
|
||||
try {
|
||||
const initialCallTime = Date.now()
|
||||
|
||||
let currentResponse = await groq.chat.completions.create(payload)
|
||||
let currentResponse = await groq.chat.completions.create(
|
||||
payload,
|
||||
request.abortSignal ? { signal: request.abortSignal } : undefined
|
||||
)
|
||||
const firstResponseTime = Date.now() - initialCallTime
|
||||
|
||||
let content = currentResponse.choices[0]?.message?.content || ''
|
||||
@@ -355,7 +361,10 @@ export const groqProvider: ProviderConfig = {
|
||||
}
|
||||
|
||||
const nextModelStartTime = Date.now()
|
||||
currentResponse = await groq.chat.completions.create(nextPayload)
|
||||
currentResponse = await groq.chat.completions.create(
|
||||
nextPayload,
|
||||
request.abortSignal ? { signal: request.abortSignal } : undefined
|
||||
)
|
||||
|
||||
const nextModelEndTime = Date.now()
|
||||
const thisModelTime = nextModelEndTime - nextModelStartTime
|
||||
@@ -396,7 +405,10 @@ export const groqProvider: ProviderConfig = {
|
||||
stream: true,
|
||||
}
|
||||
|
||||
const streamResponse = await groq.chat.completions.create(streamingPayload)
|
||||
const streamResponse = await groq.chat.completions.create(
|
||||
streamingPayload,
|
||||
request.abortSignal ? { signal: request.abortSignal } : undefined
|
||||
)
|
||||
|
||||
const accumulatedCost = calculateCost(request.model, tokens.input, tokens.output)
|
||||
|
||||
|
||||
@@ -143,7 +143,10 @@ export const mistralProvider: ProviderConfig = {
|
||||
...payload,
|
||||
stream: true,
|
||||
}
|
||||
const streamResponse = await mistral.chat.completions.create(streamingParams)
|
||||
const streamResponse = await mistral.chat.completions.create(
|
||||
streamingParams,
|
||||
request.abortSignal ? { signal: request.abortSignal } : undefined
|
||||
)
|
||||
|
||||
const streamingResult = {
|
||||
stream: createReadableStreamFromMistralStream(streamResponse, (content, usage) => {
|
||||
@@ -242,7 +245,10 @@ export const mistralProvider: ProviderConfig = {
|
||||
}
|
||||
}
|
||||
|
||||
let currentResponse = await mistral.chat.completions.create(payload)
|
||||
let currentResponse = await mistral.chat.completions.create(
|
||||
payload,
|
||||
request.abortSignal ? { signal: request.abortSignal } : undefined
|
||||
)
|
||||
const firstResponseTime = Date.now() - initialCallTime
|
||||
|
||||
let content = currentResponse.choices[0]?.message?.content || ''
|
||||
@@ -413,7 +419,10 @@ export const mistralProvider: ProviderConfig = {
|
||||
|
||||
const nextModelStartTime = Date.now()
|
||||
|
||||
currentResponse = await mistral.chat.completions.create(nextPayload)
|
||||
currentResponse = await mistral.chat.completions.create(
|
||||
nextPayload,
|
||||
request.abortSignal ? { signal: request.abortSignal } : undefined
|
||||
)
|
||||
|
||||
checkForForcedToolUsage(currentResponse, nextPayload.tool_choice)
|
||||
|
||||
@@ -454,7 +463,10 @@ export const mistralProvider: ProviderConfig = {
|
||||
tool_choice: 'auto',
|
||||
stream: true,
|
||||
}
|
||||
const streamResponse = await mistral.chat.completions.create(streamingParams)
|
||||
const streamResponse = await mistral.chat.completions.create(
|
||||
streamingParams,
|
||||
request.abortSignal ? { signal: request.abortSignal } : undefined
|
||||
)
|
||||
|
||||
const streamingResult = {
|
||||
stream: createReadableStreamFromMistralStream(streamResponse, (content, usage) => {
|
||||
|
||||
@@ -166,7 +166,10 @@ export const ollamaProvider: ProviderConfig = {
|
||||
stream: true,
|
||||
stream_options: { include_usage: true },
|
||||
}
|
||||
const streamResponse = await ollama.chat.completions.create(streamingParams)
|
||||
const streamResponse = await ollama.chat.completions.create(
|
||||
streamingParams,
|
||||
request.abortSignal ? { signal: request.abortSignal } : undefined
|
||||
)
|
||||
|
||||
const streamingResult = {
|
||||
stream: createReadableStreamFromOllamaStream(streamResponse, (content, usage) => {
|
||||
@@ -248,7 +251,10 @@ export const ollamaProvider: ProviderConfig = {
|
||||
|
||||
const initialCallTime = Date.now()
|
||||
|
||||
let currentResponse = await ollama.chat.completions.create(payload)
|
||||
let currentResponse = await ollama.chat.completions.create(
|
||||
payload,
|
||||
request.abortSignal ? { signal: request.abortSignal } : undefined
|
||||
)
|
||||
const firstResponseTime = Date.now() - initialCallTime
|
||||
|
||||
let content = currentResponse.choices[0]?.message?.content || ''
|
||||
@@ -408,7 +414,10 @@ export const ollamaProvider: ProviderConfig = {
|
||||
|
||||
const nextModelStartTime = Date.now()
|
||||
|
||||
currentResponse = await ollama.chat.completions.create(nextPayload)
|
||||
currentResponse = await ollama.chat.completions.create(
|
||||
nextPayload,
|
||||
request.abortSignal ? { signal: request.abortSignal } : undefined
|
||||
)
|
||||
|
||||
const nextModelEndTime = Date.now()
|
||||
const thisModelTime = nextModelEndTime - nextModelStartTime
|
||||
@@ -450,7 +459,10 @@ export const ollamaProvider: ProviderConfig = {
|
||||
stream: true,
|
||||
stream_options: { include_usage: true },
|
||||
}
|
||||
const streamResponse = await ollama.chat.completions.create(streamingParams)
|
||||
const streamResponse = await ollama.chat.completions.create(
|
||||
streamingParams,
|
||||
request.abortSignal ? { signal: request.abortSignal } : undefined
|
||||
)
|
||||
|
||||
const streamingResult = {
|
||||
stream: createReadableStreamFromOllamaStream(streamResponse, (content, usage) => {
|
||||
|
||||
@@ -265,6 +265,7 @@ export async function executeResponsesProviderRequest(
|
||||
method: 'POST',
|
||||
headers: config.headers,
|
||||
body: JSON.stringify(body),
|
||||
signal: request.abortSignal,
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
@@ -286,6 +287,7 @@ export async function executeResponsesProviderRequest(
|
||||
method: 'POST',
|
||||
headers: config.headers,
|
||||
body: JSON.stringify(createRequestBody(initialInput, { stream: true })),
|
||||
signal: request.abortSignal,
|
||||
})
|
||||
|
||||
if (!streamResponse.ok) {
|
||||
@@ -704,6 +706,7 @@ export async function executeResponsesProviderRequest(
|
||||
method: 'POST',
|
||||
headers: config.headers,
|
||||
body: JSON.stringify(createRequestBody(currentInput, streamOverrides)),
|
||||
signal: request.abortSignal,
|
||||
})
|
||||
|
||||
if (!streamResponse.ok) {
|
||||
|
||||
@@ -157,7 +157,10 @@ export const openRouterProvider: ProviderConfig = {
|
||||
stream: true,
|
||||
stream_options: { include_usage: true },
|
||||
}
|
||||
const streamResponse = await client.chat.completions.create(streamingParams)
|
||||
const streamResponse = await client.chat.completions.create(
|
||||
streamingParams,
|
||||
request.abortSignal ? { signal: request.abortSignal } : undefined
|
||||
)
|
||||
|
||||
const streamingResult = {
|
||||
stream: createReadableStreamFromOpenAIStream(streamResponse, (content, usage) => {
|
||||
@@ -231,7 +234,10 @@ export const openRouterProvider: ProviderConfig = {
|
||||
const forcedTools = preparedTools?.forcedTools || []
|
||||
let usedForcedTools: string[] = []
|
||||
|
||||
let currentResponse = await client.chat.completions.create(payload)
|
||||
let currentResponse = await client.chat.completions.create(
|
||||
payload,
|
||||
request.abortSignal ? { signal: request.abortSignal } : undefined
|
||||
)
|
||||
const firstResponseTime = Date.now() - initialCallTime
|
||||
|
||||
let content = currentResponse.choices[0]?.message?.content || ''
|
||||
@@ -400,7 +406,10 @@ export const openRouterProvider: ProviderConfig = {
|
||||
}
|
||||
|
||||
const nextModelStartTime = Date.now()
|
||||
currentResponse = await client.chat.completions.create(nextPayload)
|
||||
currentResponse = await client.chat.completions.create(
|
||||
nextPayload,
|
||||
request.abortSignal ? { signal: request.abortSignal } : undefined
|
||||
)
|
||||
const nextForcedToolResult = checkForForcedToolUsage(
|
||||
currentResponse,
|
||||
nextPayload.tool_choice,
|
||||
@@ -450,7 +459,10 @@ export const openRouterProvider: ProviderConfig = {
|
||||
)
|
||||
}
|
||||
|
||||
const streamResponse = await client.chat.completions.create(streamingParams)
|
||||
const streamResponse = await client.chat.completions.create(
|
||||
streamingParams,
|
||||
request.abortSignal ? { signal: request.abortSignal } : undefined
|
||||
)
|
||||
|
||||
const streamingResult = {
|
||||
stream: createReadableStreamFromOpenAIStream(streamResponse, (content, usage) => {
|
||||
@@ -533,7 +545,10 @@ export const openRouterProvider: ProviderConfig = {
|
||||
)
|
||||
|
||||
const finalStartTime = Date.now()
|
||||
const finalResponse = await client.chat.completions.create(finalPayload)
|
||||
const finalResponse = await client.chat.completions.create(
|
||||
finalPayload,
|
||||
request.abortSignal ? { signal: request.abortSignal } : undefined
|
||||
)
|
||||
const finalEndTime = Date.now()
|
||||
const finalDuration = finalEndTime - finalStartTime
|
||||
|
||||
|
||||
@@ -174,8 +174,10 @@ export interface ProviderRequest {
|
||||
verbosity?: string
|
||||
thinkingLevel?: string
|
||||
isDeployedContext?: boolean
|
||||
callChain?: string[]
|
||||
/** Previous interaction ID for multi-turn Interactions API requests (deep research follow-ups) */
|
||||
previousInteractionId?: string
|
||||
abortSignal?: AbortSignal
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1110,6 +1110,7 @@ export function prepareToolExecution(
|
||||
blockData?: Record<string, any>
|
||||
blockNameMapping?: Record<string, string>
|
||||
isDeployedContext?: boolean
|
||||
callChain?: string[]
|
||||
}
|
||||
): {
|
||||
toolParams: Record<string, any>
|
||||
@@ -1137,6 +1138,7 @@ export function prepareToolExecution(
|
||||
...(request.isDeployedContext !== undefined
|
||||
? { isDeployedContext: request.isDeployedContext }
|
||||
: {}),
|
||||
...(request.callChain ? { callChain: request.callChain } : {}),
|
||||
},
|
||||
}
|
||||
: {}),
|
||||
|
||||
@@ -189,7 +189,10 @@ export const vllmProvider: ProviderConfig = {
|
||||
stream: true,
|
||||
stream_options: { include_usage: true },
|
||||
}
|
||||
const streamResponse = await vllm.chat.completions.create(streamingParams)
|
||||
const streamResponse = await vllm.chat.completions.create(
|
||||
streamingParams,
|
||||
request.abortSignal ? { signal: request.abortSignal } : undefined
|
||||
)
|
||||
|
||||
const streamingResult = {
|
||||
stream: createReadableStreamFromVLLMStream(streamResponse, (content, usage) => {
|
||||
@@ -293,7 +296,10 @@ export const vllmProvider: ProviderConfig = {
|
||||
}
|
||||
}
|
||||
|
||||
let currentResponse = await vllm.chat.completions.create(payload)
|
||||
let currentResponse = await vllm.chat.completions.create(
|
||||
payload,
|
||||
request.abortSignal ? { signal: request.abortSignal } : undefined
|
||||
)
|
||||
const firstResponseTime = Date.now() - initialCallTime
|
||||
|
||||
let content = currentResponse.choices[0]?.message?.content || ''
|
||||
@@ -474,7 +480,10 @@ export const vllmProvider: ProviderConfig = {
|
||||
|
||||
const nextModelStartTime = Date.now()
|
||||
|
||||
currentResponse = await vllm.chat.completions.create(nextPayload)
|
||||
currentResponse = await vllm.chat.completions.create(
|
||||
nextPayload,
|
||||
request.abortSignal ? { signal: request.abortSignal } : undefined
|
||||
)
|
||||
|
||||
checkForForcedToolUsage(currentResponse, nextPayload.tool_choice)
|
||||
|
||||
@@ -519,7 +528,10 @@ export const vllmProvider: ProviderConfig = {
|
||||
stream: true,
|
||||
stream_options: { include_usage: true },
|
||||
}
|
||||
const streamResponse = await vllm.chat.completions.create(streamingParams)
|
||||
const streamResponse = await vllm.chat.completions.create(
|
||||
streamingParams,
|
||||
request.abortSignal ? { signal: request.abortSignal } : undefined
|
||||
)
|
||||
|
||||
const streamingResult = {
|
||||
stream: createReadableStreamFromVLLMStream(streamResponse, (content, usage) => {
|
||||
|
||||
@@ -115,7 +115,10 @@ export const xAIProvider: ProviderConfig = {
|
||||
}
|
||||
: { ...basePayload, stream: true, stream_options: { include_usage: true } }
|
||||
|
||||
const streamResponse = await xai.chat.completions.create(streamingParams)
|
||||
const streamResponse = await xai.chat.completions.create(
|
||||
streamingParams,
|
||||
request.abortSignal ? { signal: request.abortSignal } : undefined
|
||||
)
|
||||
|
||||
const streamingResult = {
|
||||
stream: createReadableStreamFromXAIStream(streamResponse, (content, usage) => {
|
||||
@@ -199,7 +202,10 @@ export const xAIProvider: ProviderConfig = {
|
||||
Object.assign(initialPayload, responseFormatPayload)
|
||||
}
|
||||
|
||||
let currentResponse = await xai.chat.completions.create(initialPayload)
|
||||
let currentResponse = await xai.chat.completions.create(
|
||||
initialPayload,
|
||||
request.abortSignal ? { signal: request.abortSignal } : undefined
|
||||
)
|
||||
const firstResponseTime = Date.now() - initialCallTime
|
||||
|
||||
let content = currentResponse.choices[0]?.message?.content || ''
|
||||
@@ -414,7 +420,10 @@ export const xAIProvider: ProviderConfig = {
|
||||
|
||||
const nextModelStartTime = Date.now()
|
||||
|
||||
currentResponse = await xai.chat.completions.create(nextPayload)
|
||||
currentResponse = await xai.chat.completions.create(
|
||||
nextPayload,
|
||||
request.abortSignal ? { signal: request.abortSignal } : undefined
|
||||
)
|
||||
if (nextPayload.tool_choice && typeof nextPayload.tool_choice === 'object') {
|
||||
const result = checkForForcedToolUsage(
|
||||
currentResponse,
|
||||
@@ -479,7 +488,10 @@ export const xAIProvider: ProviderConfig = {
|
||||
}
|
||||
}
|
||||
|
||||
const streamResponse = await xai.chat.completions.create(finalStreamingPayload as any)
|
||||
const streamResponse = await xai.chat.completions.create(
|
||||
finalStreamingPayload as any,
|
||||
request.abortSignal ? { signal: request.abortSignal } : undefined
|
||||
)
|
||||
|
||||
const accumulatedCost = calculateCost(request.model, tokens.input, tokens.output)
|
||||
|
||||
|
||||
@@ -683,34 +683,37 @@ describe('Serializer', () => {
|
||||
expect(slackBlock?.config.params.username).toBe('bot')
|
||||
})
|
||||
|
||||
it.concurrent('should fall back to legacy advancedMode when canonicalModes not set', () => {
|
||||
const serializer = new Serializer()
|
||||
it.concurrent(
|
||||
'should fall back to legacy advancedMode for non-credential canonical groups when canonicalModes not set',
|
||||
() => {
|
||||
const serializer = new Serializer()
|
||||
|
||||
const block: any = {
|
||||
id: 'slack-1',
|
||||
type: 'slack',
|
||||
name: 'Test Slack Block',
|
||||
position: { x: 0, y: 0 },
|
||||
advancedMode: true,
|
||||
subBlocks: {
|
||||
operation: { value: 'send' },
|
||||
destinationType: { value: 'channel' },
|
||||
channel: { value: 'general' },
|
||||
manualChannel: { value: 'C1234567890' },
|
||||
text: { value: 'Hello world' },
|
||||
username: { value: 'bot' },
|
||||
},
|
||||
outputs: {},
|
||||
enabled: true,
|
||||
const block: any = {
|
||||
id: 'slack-1',
|
||||
type: 'slack',
|
||||
name: 'Test Slack Block',
|
||||
position: { x: 0, y: 0 },
|
||||
advancedMode: true,
|
||||
subBlocks: {
|
||||
operation: { value: 'send' },
|
||||
destinationType: { value: 'channel' },
|
||||
channel: { value: 'general' },
|
||||
manualChannel: { value: 'C1234567890' },
|
||||
text: { value: 'Hello world' },
|
||||
username: { value: 'bot' },
|
||||
},
|
||||
outputs: {},
|
||||
enabled: true,
|
||||
}
|
||||
|
||||
const serialized = serializer.serializeWorkflow({ 'slack-1': block }, [], {})
|
||||
const slackBlock = serialized.blocks.find((b) => b.id === 'slack-1')
|
||||
|
||||
expect(slackBlock).toBeDefined()
|
||||
expect(slackBlock?.config.params.channel).toBe('C1234567890')
|
||||
expect(slackBlock?.config.params.manualChannel).toBeUndefined()
|
||||
}
|
||||
|
||||
const serialized = serializer.serializeWorkflow({ 'slack-1': block }, [], {})
|
||||
const slackBlock = serialized.blocks.find((b) => b.id === 'slack-1')
|
||||
|
||||
expect(slackBlock).toBeDefined()
|
||||
expect(slackBlock?.config.params.channel).toBe('C1234567890')
|
||||
expect(slackBlock?.config.params.manualChannel).toBeUndefined()
|
||||
})
|
||||
)
|
||||
|
||||
it.concurrent('should use basic value by default when no mode specified', () => {
|
||||
const serializer = new Serializer()
|
||||
|
||||
@@ -61,8 +61,9 @@ function shouldSerializeSubBlock(
|
||||
const group = canonicalId ? canonicalIndex.groupsById[canonicalId] : undefined
|
||||
if (group && isCanonicalPair(group)) {
|
||||
const mode =
|
||||
canonicalModeOverrides?.[group.canonicalId] ??
|
||||
(displayAdvancedOptions ? 'advanced' : resolveCanonicalMode(group, values))
|
||||
canonicalModeOverrides?.[group.canonicalId] != null || !displayAdvancedOptions
|
||||
? resolveCanonicalMode(group, values, canonicalModeOverrides)
|
||||
: 'advanced'
|
||||
const matchesMode =
|
||||
mode === 'advanced'
|
||||
? group.advancedIds.includes(subBlockConfig.id)
|
||||
@@ -374,8 +375,11 @@ export class Serializer {
|
||||
|
||||
Object.values(canonicalIndex.groupsById).forEach((group) => {
|
||||
const { basicValue, advancedValue } = getCanonicalValues(group, params)
|
||||
const hasExplicitOverride = canonicalModeOverrides?.[group.canonicalId] != null
|
||||
const pairMode =
|
||||
canonicalModeOverrides?.[group.canonicalId] ?? (legacyAdvancedMode ? 'advanced' : 'basic')
|
||||
hasExplicitOverride || !legacyAdvancedMode
|
||||
? resolveCanonicalMode(group, allValues, canonicalModeOverrides)
|
||||
: 'advanced'
|
||||
const chosen = pairMode === 'advanced' ? advancedValue : basicValue
|
||||
|
||||
const sourceIds = [group.basicId, ...group.advancedIds].filter(Boolean) as string[]
|
||||
|
||||
@@ -7,6 +7,8 @@ export const useSettingsModalStore = create<SettingsModalState>((set) => ({
|
||||
isOpen: false,
|
||||
initialSection: null,
|
||||
mcpServerId: null,
|
||||
hasUnsavedChanges: false,
|
||||
onCloseAttempt: null,
|
||||
|
||||
openModal: (options) =>
|
||||
set({
|
||||
@@ -18,6 +20,8 @@ export const useSettingsModalStore = create<SettingsModalState>((set) => ({
|
||||
closeModal: () =>
|
||||
set({
|
||||
isOpen: false,
|
||||
hasUnsavedChanges: false,
|
||||
onCloseAttempt: null,
|
||||
}),
|
||||
|
||||
clearInitialState: () =>
|
||||
@@ -25,4 +29,14 @@ export const useSettingsModalStore = create<SettingsModalState>((set) => ({
|
||||
initialSection: null,
|
||||
mcpServerId: null,
|
||||
}),
|
||||
|
||||
setHasUnsavedChanges: (hasChanges) =>
|
||||
set({
|
||||
hasUnsavedChanges: hasChanges,
|
||||
}),
|
||||
|
||||
setOnCloseAttempt: (callback) =>
|
||||
set({
|
||||
onCloseAttempt: callback,
|
||||
}),
|
||||
}))
|
||||
|
||||
@@ -16,8 +16,12 @@ export interface SettingsModalState {
|
||||
isOpen: boolean
|
||||
initialSection: SettingsSection | null
|
||||
mcpServerId: string | null
|
||||
hasUnsavedChanges: boolean
|
||||
onCloseAttempt: (() => void) | null
|
||||
|
||||
openModal: (options?: { section?: SettingsSection; mcpServerId?: string }) => void
|
||||
closeModal: () => void
|
||||
clearInitialState: () => void
|
||||
setHasUnsavedChanges: (hasChanges: boolean) => void
|
||||
setOnCloseAttempt: (callback: (() => void) | null) => void
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ import { v4 as uuidv4 } from 'uuid'
|
||||
import { DEFAULT_DUPLICATE_OFFSET } from '@/lib/workflows/autolayout/constants'
|
||||
import { getEffectiveBlockOutputs } from '@/lib/workflows/blocks/block-outputs'
|
||||
import { mergeSubblockStateWithValues } from '@/lib/workflows/subblocks'
|
||||
import { buildDefaultCanonicalModes } from '@/lib/workflows/subblocks/visibility'
|
||||
import { hasTriggerCapability } from '@/lib/workflows/triggers/trigger-utils'
|
||||
import { TriggerUtils } from '@/lib/workflows/triggers/triggers'
|
||||
import { getBlock } from '@/blocks'
|
||||
@@ -196,6 +197,13 @@ export function prepareBlockState(options: PrepareBlockStateOptions): BlockState
|
||||
preferToolOutputs: !effectiveTriggerMode,
|
||||
})
|
||||
|
||||
if (blockConfig.subBlocks) {
|
||||
const canonicalModes = buildDefaultCanonicalModes(blockConfig.subBlocks)
|
||||
if (Object.keys(canonicalModes).length > 0) {
|
||||
blockData.canonicalModes = canonicalModes
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
id,
|
||||
type,
|
||||
|
||||
95
apps/sim/tools/attio/assert_record.ts
Normal file
95
apps/sim/tools/attio/assert_record.ts
Normal file
@@ -0,0 +1,95 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
import type { AttioAssertRecordParams, AttioAssertRecordResponse } from './types'
|
||||
import { RECORD_OUTPUT_PROPERTIES } from './types'
|
||||
|
||||
const logger = createLogger('AttioAssertRecord')
|
||||
|
||||
export const attioAssertRecordTool: ToolConfig<AttioAssertRecordParams, AttioAssertRecordResponse> =
|
||||
{
|
||||
id: 'attio_assert_record',
|
||||
name: 'Attio Assert Record',
|
||||
description:
|
||||
'Upsert a record in Attio — creates it if no match is found, updates it if a match exists',
|
||||
version: '1.0.0',
|
||||
|
||||
oauth: {
|
||||
required: true,
|
||||
provider: 'attio',
|
||||
},
|
||||
|
||||
params: {
|
||||
accessToken: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'hidden',
|
||||
description: 'The OAuth access token for the Attio API',
|
||||
},
|
||||
objectType: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'The object type slug (e.g. people, companies)',
|
||||
},
|
||||
matchingAttribute: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description:
|
||||
'The attribute slug to match on for upsert (e.g. email_addresses for people, domains for companies)',
|
||||
},
|
||||
values: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description:
|
||||
'JSON object of attribute values (e.g. {"email_addresses":[{"email_address":"test@example.com"}]})',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) =>
|
||||
`https://api.attio.com/v2/objects/${params.objectType}/records?matching_attribute=${params.matchingAttribute}`,
|
||||
method: 'PUT',
|
||||
headers: (params) => ({
|
||||
Authorization: `Bearer ${params.accessToken}`,
|
||||
'Content-Type': 'application/json',
|
||||
}),
|
||||
body: (params) => {
|
||||
let values: Record<string, unknown> = {}
|
||||
try {
|
||||
values = typeof params.values === 'string' ? JSON.parse(params.values) : params.values
|
||||
} catch {
|
||||
values = {}
|
||||
}
|
||||
return { data: { values } }
|
||||
},
|
||||
},
|
||||
|
||||
transformResponse: async (response) => {
|
||||
const data = await response.json()
|
||||
if (!response.ok) {
|
||||
logger.error('Attio API request failed', { data, status: response.status })
|
||||
throw new Error(data.message || 'Failed to assert record')
|
||||
}
|
||||
const record = data.data
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
record,
|
||||
recordId: record.id?.record_id ?? null,
|
||||
webUrl: record.web_url ?? null,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
record: {
|
||||
type: 'object',
|
||||
description: 'The upserted record',
|
||||
properties: RECORD_OUTPUT_PROPERTIES,
|
||||
},
|
||||
recordId: { type: 'string', description: 'The record ID' },
|
||||
webUrl: { type: 'string', description: 'URL to view the record in Attio' },
|
||||
},
|
||||
}
|
||||
137
apps/sim/tools/attio/create_comment.ts
Normal file
137
apps/sim/tools/attio/create_comment.ts
Normal file
@@ -0,0 +1,137 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
import type { AttioCreateCommentParams, AttioCreateCommentResponse } from './types'
|
||||
import { COMMENT_OUTPUT_PROPERTIES } from './types'
|
||||
|
||||
const logger = createLogger('AttioCreateComment')
|
||||
|
||||
export const attioCreateCommentTool: ToolConfig<
|
||||
AttioCreateCommentParams,
|
||||
AttioCreateCommentResponse
|
||||
> = {
|
||||
id: 'attio_create_comment',
|
||||
name: 'Attio Create Comment',
|
||||
description: 'Create a comment on a list entry in Attio',
|
||||
version: '1.0.0',
|
||||
|
||||
oauth: {
|
||||
required: true,
|
||||
provider: 'attio',
|
||||
},
|
||||
|
||||
params: {
|
||||
accessToken: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'hidden',
|
||||
description: 'The OAuth access token for the Attio API',
|
||||
},
|
||||
content: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'The comment content',
|
||||
},
|
||||
format: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Content format: plaintext or markdown (default plaintext)',
|
||||
},
|
||||
authorType: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Author type (e.g. workspace-member)',
|
||||
},
|
||||
authorId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Author workspace member ID',
|
||||
},
|
||||
list: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'The list ID or slug the entry belongs to',
|
||||
},
|
||||
entryId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'The entry ID to comment on',
|
||||
},
|
||||
threadId: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Thread ID to reply to (omit to start a new thread)',
|
||||
},
|
||||
createdAt: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Backdate the comment (ISO 8601 format)',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: 'https://api.attio.com/v2/comments',
|
||||
method: 'POST',
|
||||
headers: (params) => ({
|
||||
Authorization: `Bearer ${params.accessToken}`,
|
||||
'Content-Type': 'application/json',
|
||||
}),
|
||||
body: (params) => {
|
||||
const data: Record<string, unknown> = {
|
||||
format: params.format || 'plaintext',
|
||||
content: params.content,
|
||||
author: {
|
||||
type: params.authorType,
|
||||
id: params.authorId,
|
||||
},
|
||||
entry: {
|
||||
list: params.list,
|
||||
entry_id: params.entryId,
|
||||
},
|
||||
}
|
||||
if (params.threadId) data.thread_id = params.threadId
|
||||
if (params.createdAt) data.created_at = params.createdAt
|
||||
return { data }
|
||||
},
|
||||
},
|
||||
|
||||
transformResponse: async (response) => {
|
||||
const data = await response.json()
|
||||
if (!response.ok) {
|
||||
logger.error('Attio API request failed', { data, status: response.status })
|
||||
throw new Error(data.message || 'Failed to create comment')
|
||||
}
|
||||
const c = data.data
|
||||
const author = c.author as { type?: string; id?: string } | undefined
|
||||
const entry = c.entry as { list_id?: string; entry_id?: string } | undefined
|
||||
const record = c.record as { object_id?: string; record_id?: string } | undefined
|
||||
const resolvedBy = c.resolved_by as { type?: string; id?: string } | undefined
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
commentId: c.id?.comment_id ?? null,
|
||||
threadId: c.thread_id ?? null,
|
||||
contentPlaintext: c.content_plaintext ?? null,
|
||||
author: author ? { type: author.type ?? null, id: author.id ?? null } : null,
|
||||
entry: entry ? { listId: entry.list_id ?? null, entryId: entry.entry_id ?? null } : null,
|
||||
record: record
|
||||
? { objectId: record.object_id ?? null, recordId: record.record_id ?? null }
|
||||
: null,
|
||||
resolvedAt: c.resolved_at ?? null,
|
||||
resolvedBy: resolvedBy
|
||||
? { type: resolvedBy.type ?? null, id: resolvedBy.id ?? null }
|
||||
: null,
|
||||
createdAt: c.created_at ?? null,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: COMMENT_OUTPUT_PROPERTIES,
|
||||
}
|
||||
121
apps/sim/tools/attio/create_list.ts
Normal file
121
apps/sim/tools/attio/create_list.ts
Normal file
@@ -0,0 +1,121 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
import type { AttioCreateListParams, AttioCreateListResponse } from './types'
|
||||
import { LIST_OUTPUT_PROPERTIES } from './types'
|
||||
|
||||
const logger = createLogger('AttioCreateList')
|
||||
|
||||
export const attioCreateListTool: ToolConfig<AttioCreateListParams, AttioCreateListResponse> = {
|
||||
id: 'attio_create_list',
|
||||
name: 'Attio Create List',
|
||||
description: 'Create a new list in Attio',
|
||||
version: '1.0.0',
|
||||
|
||||
oauth: {
|
||||
required: true,
|
||||
provider: 'attio',
|
||||
},
|
||||
|
||||
params: {
|
||||
accessToken: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'hidden',
|
||||
description: 'The OAuth access token for the Attio API',
|
||||
},
|
||||
name: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'The list name',
|
||||
},
|
||||
apiSlug: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'The API slug for the list (auto-generated from name if omitted)',
|
||||
},
|
||||
parentObject: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'The parent object slug (e.g. people, companies)',
|
||||
},
|
||||
workspaceAccess: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description:
|
||||
'Workspace-level access: full-access, read-and-write, or read-only (omit for private)',
|
||||
},
|
||||
workspaceMemberAccess: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description:
|
||||
'JSON array of member access entries, e.g. [{"workspace_member_id":"...","level":"read-and-write"}]',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: 'https://api.attio.com/v2/lists',
|
||||
method: 'POST',
|
||||
headers: (params) => ({
|
||||
Authorization: `Bearer ${params.accessToken}`,
|
||||
'Content-Type': 'application/json',
|
||||
}),
|
||||
body: (params) => {
|
||||
const apiSlug =
|
||||
params.apiSlug ||
|
||||
params.name
|
||||
?.toLowerCase()
|
||||
.replace(/[^a-z0-9]+/g, '_')
|
||||
.replace(/^_|_$/g, '')
|
||||
const data: Record<string, unknown> = {
|
||||
name: params.name,
|
||||
api_slug: apiSlug,
|
||||
parent_object: params.parentObject,
|
||||
workspace_access: params.workspaceAccess ?? null,
|
||||
workspace_member_access: [] as unknown[],
|
||||
}
|
||||
if (params.workspaceMemberAccess) {
|
||||
try {
|
||||
data.workspace_member_access =
|
||||
typeof params.workspaceMemberAccess === 'string'
|
||||
? JSON.parse(params.workspaceMemberAccess)
|
||||
: params.workspaceMemberAccess
|
||||
} catch {
|
||||
data.workspace_member_access = params.workspaceMemberAccess
|
||||
}
|
||||
}
|
||||
return { data }
|
||||
},
|
||||
},
|
||||
|
||||
transformResponse: async (response) => {
|
||||
const data = await response.json()
|
||||
if (!response.ok) {
|
||||
logger.error('Attio API request failed', { data, status: response.status })
|
||||
throw new Error(data.message || 'Failed to create list')
|
||||
}
|
||||
const list = data.data
|
||||
const actor = list.created_by_actor as { type?: string; id?: string } | undefined
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
listId: list.id?.list_id ?? null,
|
||||
apiSlug: list.api_slug ?? null,
|
||||
name: list.name ?? null,
|
||||
parentObject: Array.isArray(list.parent_object)
|
||||
? (list.parent_object[0] ?? null)
|
||||
: (list.parent_object ?? null),
|
||||
workspaceAccess: list.workspace_access ?? null,
|
||||
workspaceMemberAccess: list.workspace_member_access ?? null,
|
||||
createdByActor: actor ? { type: actor.type ?? null, id: actor.id ?? null } : null,
|
||||
createdAt: list.created_at ?? null,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: LIST_OUTPUT_PROPERTIES,
|
||||
}
|
||||
104
apps/sim/tools/attio/create_list_entry.ts
Normal file
104
apps/sim/tools/attio/create_list_entry.ts
Normal file
@@ -0,0 +1,104 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
import type { AttioCreateListEntryParams, AttioCreateListEntryResponse } from './types'
|
||||
import { LIST_ENTRY_OUTPUT_PROPERTIES } from './types'
|
||||
|
||||
const logger = createLogger('AttioCreateListEntry')
|
||||
|
||||
export const attioCreateListEntryTool: ToolConfig<
|
||||
AttioCreateListEntryParams,
|
||||
AttioCreateListEntryResponse
|
||||
> = {
|
||||
id: 'attio_create_list_entry',
|
||||
name: 'Attio Create List Entry',
|
||||
description: 'Add a record to an Attio list as a new entry',
|
||||
version: '1.0.0',
|
||||
|
||||
oauth: {
|
||||
required: true,
|
||||
provider: 'attio',
|
||||
},
|
||||
|
||||
params: {
|
||||
accessToken: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'hidden',
|
||||
description: 'The OAuth access token for the Attio API',
|
||||
},
|
||||
list: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'The list ID or slug',
|
||||
},
|
||||
parentRecordId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'The record ID to add to the list',
|
||||
},
|
||||
parentObject: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'The object type slug of the record (e.g. people, companies)',
|
||||
},
|
||||
entryValues: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'JSON object of entry attribute values',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => `https://api.attio.com/v2/lists/${params.list}/entries`,
|
||||
method: 'POST',
|
||||
headers: (params) => ({
|
||||
Authorization: `Bearer ${params.accessToken}`,
|
||||
'Content-Type': 'application/json',
|
||||
}),
|
||||
body: (params) => {
|
||||
let entryValues: unknown = {}
|
||||
if (params.entryValues) {
|
||||
try {
|
||||
entryValues =
|
||||
typeof params.entryValues === 'string'
|
||||
? JSON.parse(params.entryValues)
|
||||
: params.entryValues
|
||||
} catch {
|
||||
entryValues = {}
|
||||
}
|
||||
}
|
||||
const data: Record<string, unknown> = {
|
||||
parent_record_id: params.parentRecordId,
|
||||
parent_object: params.parentObject,
|
||||
entry_values: entryValues,
|
||||
}
|
||||
return { data }
|
||||
},
|
||||
},
|
||||
|
||||
transformResponse: async (response) => {
|
||||
const data = await response.json()
|
||||
if (!response.ok) {
|
||||
logger.error('Attio API request failed', { data, status: response.status })
|
||||
throw new Error(data.message || 'Failed to create list entry')
|
||||
}
|
||||
const entry = data.data
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
entryId: entry.id?.entry_id ?? null,
|
||||
listId: entry.id?.list_id ?? null,
|
||||
parentRecordId: entry.parent_record_id ?? null,
|
||||
parentObject: entry.parent_object ?? null,
|
||||
createdAt: entry.created_at ?? null,
|
||||
entryValues: entry.entry_values ?? {},
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: LIST_ENTRY_OUTPUT_PROPERTIES,
|
||||
}
|
||||
116
apps/sim/tools/attio/create_note.ts
Normal file
116
apps/sim/tools/attio/create_note.ts
Normal file
@@ -0,0 +1,116 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
import type { AttioCreateNoteParams, AttioCreateNoteResponse } from './types'
|
||||
import { NOTE_OUTPUT_PROPERTIES } from './types'
|
||||
|
||||
const logger = createLogger('AttioCreateNote')
|
||||
|
||||
export const attioCreateNoteTool: ToolConfig<AttioCreateNoteParams, AttioCreateNoteResponse> = {
|
||||
id: 'attio_create_note',
|
||||
name: 'Attio Create Note',
|
||||
description: 'Create a note on a record in Attio',
|
||||
version: '1.0.0',
|
||||
|
||||
oauth: {
|
||||
required: true,
|
||||
provider: 'attio',
|
||||
},
|
||||
|
||||
params: {
|
||||
accessToken: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'hidden',
|
||||
description: 'The OAuth access token for the Attio API',
|
||||
},
|
||||
parentObject: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'The parent object type slug (e.g. people, companies)',
|
||||
},
|
||||
parentRecordId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'The parent record ID to attach the note to',
|
||||
},
|
||||
title: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'The note title',
|
||||
},
|
||||
content: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'The note content',
|
||||
},
|
||||
format: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Content format: plaintext or markdown (default plaintext)',
|
||||
},
|
||||
createdAt: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Backdate the note creation time (ISO 8601 format)',
|
||||
},
|
||||
meetingId: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Associate the note with a meeting ID',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: 'https://api.attio.com/v2/notes',
|
||||
method: 'POST',
|
||||
headers: (params) => ({
|
||||
Authorization: `Bearer ${params.accessToken}`,
|
||||
'Content-Type': 'application/json',
|
||||
}),
|
||||
body: (params) => {
|
||||
const body: Record<string, unknown> = {
|
||||
parent_object: params.parentObject,
|
||||
parent_record_id: params.parentRecordId,
|
||||
title: params.title,
|
||||
format: params.format || 'plaintext',
|
||||
content: params.content,
|
||||
}
|
||||
if (params.createdAt) body.created_at = params.createdAt
|
||||
if (params.meetingId != null) body.meeting_id = params.meetingId || null
|
||||
return { data: body }
|
||||
},
|
||||
},
|
||||
|
||||
transformResponse: async (response) => {
|
||||
const data = await response.json()
|
||||
if (!response.ok) {
|
||||
logger.error('Attio API request failed', { data, status: response.status })
|
||||
throw new Error(data.message || 'Failed to create note')
|
||||
}
|
||||
const note = data.data
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
noteId: note.id?.note_id ?? null,
|
||||
parentObject: note.parent_object ?? null,
|
||||
parentRecordId: note.parent_record_id ?? null,
|
||||
title: note.title ?? null,
|
||||
contentPlaintext: note.content_plaintext ?? null,
|
||||
contentMarkdown: note.content_markdown ?? null,
|
||||
meetingId: note.meeting_id ?? null,
|
||||
tags: note.tags ?? [],
|
||||
createdByActor: note.created_by_actor ?? null,
|
||||
createdAt: note.created_at ?? null,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: NOTE_OUTPUT_PROPERTIES,
|
||||
}
|
||||
83
apps/sim/tools/attio/create_object.ts
Normal file
83
apps/sim/tools/attio/create_object.ts
Normal file
@@ -0,0 +1,83 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
import type { AttioCreateObjectParams, AttioCreateObjectResponse } from './types'
|
||||
import { OBJECT_OUTPUT_PROPERTIES } from './types'
|
||||
|
||||
const logger = createLogger('AttioCreateObject')
|
||||
|
||||
export const attioCreateObjectTool: ToolConfig<AttioCreateObjectParams, AttioCreateObjectResponse> =
|
||||
{
|
||||
id: 'attio_create_object',
|
||||
name: 'Attio Create Object',
|
||||
description: 'Create a custom object in Attio',
|
||||
version: '1.0.0',
|
||||
|
||||
oauth: {
|
||||
required: true,
|
||||
provider: 'attio',
|
||||
},
|
||||
|
||||
params: {
|
||||
accessToken: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'hidden',
|
||||
description: 'The OAuth access token for the Attio API',
|
||||
},
|
||||
apiSlug: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'The API slug for the object (e.g. projects)',
|
||||
},
|
||||
singularNoun: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Singular display name (e.g. Project)',
|
||||
},
|
||||
pluralNoun: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Plural display name (e.g. Projects)',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: 'https://api.attio.com/v2/objects',
|
||||
method: 'POST',
|
||||
headers: (params) => ({
|
||||
Authorization: `Bearer ${params.accessToken}`,
|
||||
'Content-Type': 'application/json',
|
||||
}),
|
||||
body: (params) => ({
|
||||
data: {
|
||||
api_slug: params.apiSlug,
|
||||
singular_noun: params.singularNoun,
|
||||
plural_noun: params.pluralNoun,
|
||||
},
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response) => {
|
||||
const data = await response.json()
|
||||
if (!response.ok) {
|
||||
logger.error('Attio API request failed', { data, status: response.status })
|
||||
throw new Error(data.message || 'Failed to create object')
|
||||
}
|
||||
const obj = data.data
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
objectId: obj.id?.object_id ?? null,
|
||||
apiSlug: obj.api_slug ?? null,
|
||||
singularNoun: obj.singular_noun ?? null,
|
||||
pluralNoun: obj.plural_noun ?? null,
|
||||
createdAt: obj.created_at ?? null,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: OBJECT_OUTPUT_PROPERTIES,
|
||||
}
|
||||
81
apps/sim/tools/attio/create_record.ts
Normal file
81
apps/sim/tools/attio/create_record.ts
Normal file
@@ -0,0 +1,81 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
import type { AttioCreateRecordParams, AttioCreateRecordResponse } from './types'
|
||||
import { RECORD_OBJECT_OUTPUT } from './types'
|
||||
|
||||
const logger = createLogger('AttioCreateRecord')
|
||||
|
||||
export const attioCreateRecordTool: ToolConfig<AttioCreateRecordParams, AttioCreateRecordResponse> =
|
||||
{
|
||||
id: 'attio_create_record',
|
||||
name: 'Attio Create Record',
|
||||
description: 'Create a new record in Attio for a given object type',
|
||||
version: '1.0.0',
|
||||
|
||||
oauth: {
|
||||
required: true,
|
||||
provider: 'attio',
|
||||
},
|
||||
|
||||
params: {
|
||||
accessToken: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'hidden',
|
||||
description: 'The OAuth access token for the Attio API',
|
||||
},
|
||||
objectType: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'The object type slug (e.g. people, companies)',
|
||||
},
|
||||
values: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'JSON object of attribute values to set on the record',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => `https://api.attio.com/v2/objects/${params.objectType}/records`,
|
||||
method: 'POST',
|
||||
headers: (params) => ({
|
||||
Authorization: `Bearer ${params.accessToken}`,
|
||||
'Content-Type': 'application/json',
|
||||
}),
|
||||
body: (params) => {
|
||||
let values: Record<string, unknown>
|
||||
try {
|
||||
values = typeof params.values === 'string' ? JSON.parse(params.values) : params.values
|
||||
} catch {
|
||||
values = {}
|
||||
}
|
||||
return { data: { values } }
|
||||
},
|
||||
},
|
||||
|
||||
transformResponse: async (response) => {
|
||||
const data = await response.json()
|
||||
if (!response.ok) {
|
||||
logger.error('Attio API request failed', { data, status: response.status })
|
||||
throw new Error(data.message || 'Failed to create record')
|
||||
}
|
||||
const record = data.data
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
record,
|
||||
recordId: record.id?.record_id ?? null,
|
||||
webUrl: record.web_url ?? null,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
record: RECORD_OBJECT_OUTPUT,
|
||||
recordId: { type: 'string', description: 'The ID of the created record' },
|
||||
webUrl: { type: 'string', description: 'URL to view the record in Attio' },
|
||||
},
|
||||
}
|
||||
136
apps/sim/tools/attio/create_task.ts
Normal file
136
apps/sim/tools/attio/create_task.ts
Normal file
@@ -0,0 +1,136 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
import type { AttioCreateTaskParams, AttioCreateTaskResponse } from './types'
|
||||
import { TASK_OUTPUT_PROPERTIES } from './types'
|
||||
|
||||
const logger = createLogger('AttioCreateTask')
|
||||
|
||||
export const attioCreateTaskTool: ToolConfig<AttioCreateTaskParams, AttioCreateTaskResponse> = {
|
||||
id: 'attio_create_task',
|
||||
name: 'Attio Create Task',
|
||||
description: 'Create a task in Attio',
|
||||
version: '1.0.0',
|
||||
|
||||
oauth: {
|
||||
required: true,
|
||||
provider: 'attio',
|
||||
},
|
||||
|
||||
params: {
|
||||
accessToken: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'hidden',
|
||||
description: 'The OAuth access token for the Attio API',
|
||||
},
|
||||
content: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'The task content (max 2000 characters)',
|
||||
},
|
||||
deadlineAt: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Deadline in ISO 8601 format (e.g. 2024-12-01T15:00:00.000Z)',
|
||||
},
|
||||
isCompleted: {
|
||||
type: 'boolean',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Whether the task is completed (default false)',
|
||||
},
|
||||
linkedRecords: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description:
|
||||
'JSON array of linked records (e.g. [{"target_object":"people","target_record_id":"..."}])',
|
||||
},
|
||||
assignees: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description:
|
||||
'JSON array of assignees (e.g. [{"referenced_actor_type":"workspace-member","referenced_actor_id":"..."}])',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: 'https://api.attio.com/v2/tasks',
|
||||
method: 'POST',
|
||||
headers: (params) => ({
|
||||
Authorization: `Bearer ${params.accessToken}`,
|
||||
'Content-Type': 'application/json',
|
||||
}),
|
||||
body: (params) => {
|
||||
let linkedRecords: unknown[] = []
|
||||
let assignees: unknown[] = []
|
||||
try {
|
||||
if (params.linkedRecords) {
|
||||
linkedRecords =
|
||||
typeof params.linkedRecords === 'string'
|
||||
? JSON.parse(params.linkedRecords)
|
||||
: params.linkedRecords
|
||||
}
|
||||
} catch {
|
||||
linkedRecords = []
|
||||
}
|
||||
try {
|
||||
if (params.assignees) {
|
||||
assignees =
|
||||
typeof params.assignees === 'string' ? JSON.parse(params.assignees) : params.assignees
|
||||
}
|
||||
} catch {
|
||||
assignees = []
|
||||
}
|
||||
return {
|
||||
data: {
|
||||
content: params.content,
|
||||
format: 'plaintext',
|
||||
deadline_at: params.deadlineAt || null,
|
||||
is_completed: params.isCompleted ?? false,
|
||||
linked_records: linkedRecords,
|
||||
assignees,
|
||||
},
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
transformResponse: async (response) => {
|
||||
const data = await response.json()
|
||||
if (!response.ok) {
|
||||
logger.error('Attio API request failed', { data, status: response.status })
|
||||
throw new Error(data.message || 'Failed to create task')
|
||||
}
|
||||
const task = data.data
|
||||
const linkedRecords = (task.linked_records ?? []).map(
|
||||
(r: { target_object_id?: string; target_record_id?: string }) => ({
|
||||
targetObjectId: r.target_object_id ?? null,
|
||||
targetRecordId: r.target_record_id ?? null,
|
||||
})
|
||||
)
|
||||
const assignees = (task.assignees ?? []).map(
|
||||
(a: { referenced_actor_type?: string; referenced_actor_id?: string }) => ({
|
||||
type: a.referenced_actor_type ?? null,
|
||||
id: a.referenced_actor_id ?? null,
|
||||
})
|
||||
)
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
taskId: task.id?.task_id ?? null,
|
||||
content: task.content_plaintext ?? null,
|
||||
deadlineAt: task.deadline_at ?? null,
|
||||
isCompleted: task.is_completed ?? false,
|
||||
linkedRecords,
|
||||
assignees,
|
||||
createdByActor: task.created_by_actor ?? null,
|
||||
createdAt: task.created_at ?? null,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: TASK_OUTPUT_PROPERTIES,
|
||||
}
|
||||
104
apps/sim/tools/attio/create_webhook.ts
Normal file
104
apps/sim/tools/attio/create_webhook.ts
Normal file
@@ -0,0 +1,104 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
import type { AttioCreateWebhookParams, AttioCreateWebhookResponse } from './types'
|
||||
import { WEBHOOK_OUTPUT_PROPERTIES } from './types'
|
||||
|
||||
const logger = createLogger('AttioCreateWebhook')
|
||||
|
||||
export const attioCreateWebhookTool: ToolConfig<
|
||||
AttioCreateWebhookParams,
|
||||
AttioCreateWebhookResponse
|
||||
> = {
|
||||
id: 'attio_create_webhook',
|
||||
name: 'Attio Create Webhook',
|
||||
description: 'Create a webhook in Attio to receive event notifications',
|
||||
version: '1.0.0',
|
||||
|
||||
oauth: {
|
||||
required: true,
|
||||
provider: 'attio',
|
||||
},
|
||||
|
||||
params: {
|
||||
accessToken: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'hidden',
|
||||
description: 'The OAuth access token for the Attio API',
|
||||
},
|
||||
targetUrl: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'The HTTPS URL to receive webhook events',
|
||||
},
|
||||
subscriptions: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description:
|
||||
'JSON array of subscriptions (e.g. [{"event_type":"record.created","filter":{"object_id":"..."}}])',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: 'https://api.attio.com/v2/webhooks',
|
||||
method: 'POST',
|
||||
headers: (params) => ({
|
||||
Authorization: `Bearer ${params.accessToken}`,
|
||||
'Content-Type': 'application/json',
|
||||
}),
|
||||
body: (params) => {
|
||||
let subscriptions: unknown[] = []
|
||||
try {
|
||||
subscriptions =
|
||||
typeof params.subscriptions === 'string'
|
||||
? JSON.parse(params.subscriptions)
|
||||
: params.subscriptions
|
||||
} catch {
|
||||
subscriptions = []
|
||||
}
|
||||
return {
|
||||
data: {
|
||||
target_url: params.targetUrl,
|
||||
subscriptions,
|
||||
},
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
transformResponse: async (response) => {
|
||||
const data = await response.json()
|
||||
if (!response.ok) {
|
||||
logger.error('Attio API request failed', { data, status: response.status })
|
||||
throw new Error(data.message || 'Failed to create webhook')
|
||||
}
|
||||
const w = data.data
|
||||
const subs =
|
||||
(w.subscriptions as Array<{ event_type?: string; filter?: unknown }>)?.map(
|
||||
(s: { event_type?: string; filter?: unknown }) => ({
|
||||
eventType: s.event_type ?? null,
|
||||
filter: s.filter ?? null,
|
||||
})
|
||||
) ?? []
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
webhookId: w.id?.webhook_id ?? null,
|
||||
targetUrl: w.target_url ?? null,
|
||||
subscriptions: subs,
|
||||
status: w.status ?? null,
|
||||
secret: w.secret ?? null,
|
||||
createdAt: w.created_at ?? null,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
...WEBHOOK_OUTPUT_PROPERTIES,
|
||||
secret: {
|
||||
type: 'string',
|
||||
description: 'The webhook signing secret (only returned on creation)',
|
||||
},
|
||||
},
|
||||
}
|
||||
61
apps/sim/tools/attio/delete_comment.ts
Normal file
61
apps/sim/tools/attio/delete_comment.ts
Normal file
@@ -0,0 +1,61 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
import type { AttioDeleteCommentParams, AttioDeleteCommentResponse } from './types'
|
||||
|
||||
const logger = createLogger('AttioDeleteComment')
|
||||
|
||||
export const attioDeleteCommentTool: ToolConfig<
|
||||
AttioDeleteCommentParams,
|
||||
AttioDeleteCommentResponse
|
||||
> = {
|
||||
id: 'attio_delete_comment',
|
||||
name: 'Attio Delete Comment',
|
||||
description: 'Delete a comment in Attio (if head of thread, deletes entire thread)',
|
||||
version: '1.0.0',
|
||||
|
||||
oauth: {
|
||||
required: true,
|
||||
provider: 'attio',
|
||||
},
|
||||
|
||||
params: {
|
||||
accessToken: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'hidden',
|
||||
description: 'The OAuth access token for the Attio API',
|
||||
},
|
||||
commentId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'The comment ID to delete',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => `https://api.attio.com/v2/comments/${params.commentId}`,
|
||||
method: 'DELETE',
|
||||
headers: (params) => ({
|
||||
Authorization: `Bearer ${params.accessToken}`,
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response) => {
|
||||
if (!response.ok) {
|
||||
const data = await response.json()
|
||||
logger.error('Attio API request failed', { data, status: response.status })
|
||||
throw new Error(data.message || 'Failed to delete comment')
|
||||
}
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
deleted: true,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
deleted: { type: 'boolean', description: 'Whether the comment was deleted' },
|
||||
},
|
||||
}
|
||||
67
apps/sim/tools/attio/delete_list_entry.ts
Normal file
67
apps/sim/tools/attio/delete_list_entry.ts
Normal file
@@ -0,0 +1,67 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
import type { AttioDeleteListEntryParams, AttioDeleteListEntryResponse } from './types'
|
||||
|
||||
const logger = createLogger('AttioDeleteListEntry')
|
||||
|
||||
export const attioDeleteListEntryTool: ToolConfig<
|
||||
AttioDeleteListEntryParams,
|
||||
AttioDeleteListEntryResponse
|
||||
> = {
|
||||
id: 'attio_delete_list_entry',
|
||||
name: 'Attio Delete List Entry',
|
||||
description: 'Remove an entry from an Attio list',
|
||||
version: '1.0.0',
|
||||
|
||||
oauth: {
|
||||
required: true,
|
||||
provider: 'attio',
|
||||
},
|
||||
|
||||
params: {
|
||||
accessToken: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'hidden',
|
||||
description: 'The OAuth access token for the Attio API',
|
||||
},
|
||||
list: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'The list ID or slug',
|
||||
},
|
||||
entryId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'The entry ID to delete',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => `https://api.attio.com/v2/lists/${params.list}/entries/${params.entryId}`,
|
||||
method: 'DELETE',
|
||||
headers: (params) => ({
|
||||
Authorization: `Bearer ${params.accessToken}`,
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response) => {
|
||||
if (!response.ok) {
|
||||
const data = await response.json()
|
||||
logger.error('Attio API request failed', { data, status: response.status })
|
||||
throw new Error(data.message || 'Failed to delete list entry')
|
||||
}
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
deleted: true,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
deleted: { type: 'boolean', description: 'Whether the entry was deleted' },
|
||||
},
|
||||
}
|
||||
58
apps/sim/tools/attio/delete_note.ts
Normal file
58
apps/sim/tools/attio/delete_note.ts
Normal file
@@ -0,0 +1,58 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
import type { AttioDeleteNoteParams, AttioDeleteNoteResponse } from './types'
|
||||
|
||||
const logger = createLogger('AttioDeleteNote')
|
||||
|
||||
export const attioDeleteNoteTool: ToolConfig<AttioDeleteNoteParams, AttioDeleteNoteResponse> = {
|
||||
id: 'attio_delete_note',
|
||||
name: 'Attio Delete Note',
|
||||
description: 'Delete a note from Attio',
|
||||
version: '1.0.0',
|
||||
|
||||
oauth: {
|
||||
required: true,
|
||||
provider: 'attio',
|
||||
},
|
||||
|
||||
params: {
|
||||
accessToken: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'hidden',
|
||||
description: 'The OAuth access token for the Attio API',
|
||||
},
|
||||
noteId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'The ID of the note to delete',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => `https://api.attio.com/v2/notes/${params.noteId}`,
|
||||
method: 'DELETE',
|
||||
headers: (params) => ({
|
||||
Authorization: `Bearer ${params.accessToken}`,
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response) => {
|
||||
if (!response.ok) {
|
||||
const data = await response.json()
|
||||
logger.error('Attio API request failed', { data, status: response.status })
|
||||
throw new Error(data.message || 'Failed to delete note')
|
||||
}
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
deleted: true,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
deleted: { type: 'boolean', description: 'Whether the note was deleted' },
|
||||
},
|
||||
}
|
||||
66
apps/sim/tools/attio/delete_record.ts
Normal file
66
apps/sim/tools/attio/delete_record.ts
Normal file
@@ -0,0 +1,66 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
import type { AttioDeleteRecordParams, AttioDeleteRecordResponse } from './types'
|
||||
|
||||
const logger = createLogger('AttioDeleteRecord')
|
||||
|
||||
export const attioDeleteRecordTool: ToolConfig<AttioDeleteRecordParams, AttioDeleteRecordResponse> =
|
||||
{
|
||||
id: 'attio_delete_record',
|
||||
name: 'Attio Delete Record',
|
||||
description: 'Delete a record from Attio',
|
||||
version: '1.0.0',
|
||||
|
||||
oauth: {
|
||||
required: true,
|
||||
provider: 'attio',
|
||||
},
|
||||
|
||||
params: {
|
||||
accessToken: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'hidden',
|
||||
description: 'The OAuth access token for the Attio API',
|
||||
},
|
||||
objectType: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'The object type slug (e.g. people, companies)',
|
||||
},
|
||||
recordId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'The ID of the record to delete',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) =>
|
||||
`https://api.attio.com/v2/objects/${params.objectType}/records/${params.recordId}`,
|
||||
method: 'DELETE',
|
||||
headers: (params) => ({
|
||||
Authorization: `Bearer ${params.accessToken}`,
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response) => {
|
||||
if (!response.ok) {
|
||||
const data = await response.json()
|
||||
logger.error('Attio API request failed', { data, status: response.status })
|
||||
throw new Error(data.message || 'Failed to delete record')
|
||||
}
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
deleted: true,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
deleted: { type: 'boolean', description: 'Whether the record was deleted' },
|
||||
},
|
||||
}
|
||||
58
apps/sim/tools/attio/delete_task.ts
Normal file
58
apps/sim/tools/attio/delete_task.ts
Normal file
@@ -0,0 +1,58 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
import type { AttioDeleteTaskParams, AttioDeleteTaskResponse } from './types'
|
||||
|
||||
const logger = createLogger('AttioDeleteTask')
|
||||
|
||||
export const attioDeleteTaskTool: ToolConfig<AttioDeleteTaskParams, AttioDeleteTaskResponse> = {
|
||||
id: 'attio_delete_task',
|
||||
name: 'Attio Delete Task',
|
||||
description: 'Delete a task from Attio',
|
||||
version: '1.0.0',
|
||||
|
||||
oauth: {
|
||||
required: true,
|
||||
provider: 'attio',
|
||||
},
|
||||
|
||||
params: {
|
||||
accessToken: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'hidden',
|
||||
description: 'The OAuth access token for the Attio API',
|
||||
},
|
||||
taskId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'The ID of the task to delete',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => `https://api.attio.com/v2/tasks/${params.taskId}`,
|
||||
method: 'DELETE',
|
||||
headers: (params) => ({
|
||||
Authorization: `Bearer ${params.accessToken}`,
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response) => {
|
||||
if (!response.ok) {
|
||||
const data = await response.json()
|
||||
logger.error('Attio API request failed', { data, status: response.status })
|
||||
throw new Error(data.message || 'Failed to delete task')
|
||||
}
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
deleted: true,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
deleted: { type: 'boolean', description: 'Whether the task was deleted' },
|
||||
},
|
||||
}
|
||||
61
apps/sim/tools/attio/delete_webhook.ts
Normal file
61
apps/sim/tools/attio/delete_webhook.ts
Normal file
@@ -0,0 +1,61 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
import type { AttioDeleteWebhookParams, AttioDeleteWebhookResponse } from './types'
|
||||
|
||||
const logger = createLogger('AttioDeleteWebhook')
|
||||
|
||||
export const attioDeleteWebhookTool: ToolConfig<
|
||||
AttioDeleteWebhookParams,
|
||||
AttioDeleteWebhookResponse
|
||||
> = {
|
||||
id: 'attio_delete_webhook',
|
||||
name: 'Attio Delete Webhook',
|
||||
description: 'Delete a webhook from Attio',
|
||||
version: '1.0.0',
|
||||
|
||||
oauth: {
|
||||
required: true,
|
||||
provider: 'attio',
|
||||
},
|
||||
|
||||
params: {
|
||||
accessToken: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'hidden',
|
||||
description: 'The OAuth access token for the Attio API',
|
||||
},
|
||||
webhookId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'The webhook ID to delete',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => `https://api.attio.com/v2/webhooks/${params.webhookId}`,
|
||||
method: 'DELETE',
|
||||
headers: (params) => ({
|
||||
Authorization: `Bearer ${params.accessToken}`,
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response) => {
|
||||
if (!response.ok) {
|
||||
const data = await response.json()
|
||||
logger.error('Attio API request failed', { data, status: response.status })
|
||||
throw new Error(data.message || 'Failed to delete webhook')
|
||||
}
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
deleted: true,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
deleted: { type: 'boolean', description: 'Whether the webhook was deleted' },
|
||||
},
|
||||
}
|
||||
74
apps/sim/tools/attio/get_comment.ts
Normal file
74
apps/sim/tools/attio/get_comment.ts
Normal file
@@ -0,0 +1,74 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
import type { AttioGetCommentParams, AttioGetCommentResponse } from './types'
|
||||
import { COMMENT_OUTPUT_PROPERTIES } from './types'
|
||||
|
||||
const logger = createLogger('AttioGetComment')
|
||||
|
||||
export const attioGetCommentTool: ToolConfig<AttioGetCommentParams, AttioGetCommentResponse> = {
|
||||
id: 'attio_get_comment',
|
||||
name: 'Attio Get Comment',
|
||||
description: 'Get a single comment by ID from Attio',
|
||||
version: '1.0.0',
|
||||
|
||||
oauth: {
|
||||
required: true,
|
||||
provider: 'attio',
|
||||
},
|
||||
|
||||
params: {
|
||||
accessToken: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'hidden',
|
||||
description: 'The OAuth access token for the Attio API',
|
||||
},
|
||||
commentId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'The comment ID',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => `https://api.attio.com/v2/comments/${params.commentId}`,
|
||||
method: 'GET',
|
||||
headers: (params) => ({
|
||||
Authorization: `Bearer ${params.accessToken}`,
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response) => {
|
||||
const data = await response.json()
|
||||
if (!response.ok) {
|
||||
logger.error('Attio API request failed', { data, status: response.status })
|
||||
throw new Error(data.message || 'Failed to get comment')
|
||||
}
|
||||
const c = data.data
|
||||
const author = c.author as { type?: string; id?: string } | undefined
|
||||
const entry = c.entry as { list_id?: string; entry_id?: string } | undefined
|
||||
const record = c.record as { object_id?: string; record_id?: string } | undefined
|
||||
const resolvedBy = c.resolved_by as { type?: string; id?: string } | undefined
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
commentId: c.id?.comment_id ?? null,
|
||||
threadId: c.thread_id ?? null,
|
||||
contentPlaintext: c.content_plaintext ?? null,
|
||||
author: author ? { type: author.type ?? null, id: author.id ?? null } : null,
|
||||
entry: entry ? { listId: entry.list_id ?? null, entryId: entry.entry_id ?? null } : null,
|
||||
record: record
|
||||
? { objectId: record.object_id ?? null, recordId: record.record_id ?? null }
|
||||
: null,
|
||||
resolvedAt: c.resolved_at ?? null,
|
||||
resolvedBy: resolvedBy
|
||||
? { type: resolvedBy.type ?? null, id: resolvedBy.id ?? null }
|
||||
: null,
|
||||
createdAt: c.created_at ?? null,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: COMMENT_OUTPUT_PROPERTIES,
|
||||
}
|
||||
68
apps/sim/tools/attio/get_list.ts
Normal file
68
apps/sim/tools/attio/get_list.ts
Normal file
@@ -0,0 +1,68 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
import type { AttioGetListParams, AttioGetListResponse } from './types'
|
||||
import { LIST_OUTPUT_PROPERTIES } from './types'
|
||||
|
||||
const logger = createLogger('AttioGetList')
|
||||
|
||||
export const attioGetListTool: ToolConfig<AttioGetListParams, AttioGetListResponse> = {
|
||||
id: 'attio_get_list',
|
||||
name: 'Attio Get List',
|
||||
description: 'Get a single list by ID or slug',
|
||||
version: '1.0.0',
|
||||
|
||||
oauth: {
|
||||
required: true,
|
||||
provider: 'attio',
|
||||
},
|
||||
|
||||
params: {
|
||||
accessToken: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'hidden',
|
||||
description: 'The OAuth access token for the Attio API',
|
||||
},
|
||||
list: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'The list ID or slug',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => `https://api.attio.com/v2/lists/${params.list}`,
|
||||
method: 'GET',
|
||||
headers: (params) => ({
|
||||
Authorization: `Bearer ${params.accessToken}`,
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response) => {
|
||||
const data = await response.json()
|
||||
if (!response.ok) {
|
||||
logger.error('Attio API request failed', { data, status: response.status })
|
||||
throw new Error(data.message || 'Failed to get list')
|
||||
}
|
||||
const list = data.data
|
||||
const actor = list.created_by_actor as { type?: string; id?: string } | undefined
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
listId: list.id?.list_id ?? null,
|
||||
apiSlug: list.api_slug ?? null,
|
||||
name: list.name ?? null,
|
||||
parentObject: Array.isArray(list.parent_object)
|
||||
? (list.parent_object[0] ?? null)
|
||||
: (list.parent_object ?? null),
|
||||
workspaceAccess: list.workspace_access ?? null,
|
||||
workspaceMemberAccess: list.workspace_member_access ?? null,
|
||||
createdByActor: actor ? { type: actor.type ?? null, id: actor.id ?? null } : null,
|
||||
createdAt: list.created_at ?? null,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: LIST_OUTPUT_PROPERTIES,
|
||||
}
|
||||
70
apps/sim/tools/attio/get_list_entry.ts
Normal file
70
apps/sim/tools/attio/get_list_entry.ts
Normal file
@@ -0,0 +1,70 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
import type { AttioGetListEntryParams, AttioGetListEntryResponse } from './types'
|
||||
import { LIST_ENTRY_OUTPUT_PROPERTIES } from './types'
|
||||
|
||||
const logger = createLogger('AttioGetListEntry')
|
||||
|
||||
export const attioGetListEntryTool: ToolConfig<AttioGetListEntryParams, AttioGetListEntryResponse> =
|
||||
{
|
||||
id: 'attio_get_list_entry',
|
||||
name: 'Attio Get List Entry',
|
||||
description: 'Get a single list entry by ID',
|
||||
version: '1.0.0',
|
||||
|
||||
oauth: {
|
||||
required: true,
|
||||
provider: 'attio',
|
||||
},
|
||||
|
||||
params: {
|
||||
accessToken: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'hidden',
|
||||
description: 'The OAuth access token for the Attio API',
|
||||
},
|
||||
list: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'The list ID or slug',
|
||||
},
|
||||
entryId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'The entry ID',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => `https://api.attio.com/v2/lists/${params.list}/entries/${params.entryId}`,
|
||||
method: 'GET',
|
||||
headers: (params) => ({
|
||||
Authorization: `Bearer ${params.accessToken}`,
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response) => {
|
||||
const data = await response.json()
|
||||
if (!response.ok) {
|
||||
logger.error('Attio API request failed', { data, status: response.status })
|
||||
throw new Error(data.message || 'Failed to get list entry')
|
||||
}
|
||||
const entry = data.data
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
entryId: entry.id?.entry_id ?? null,
|
||||
listId: entry.id?.list_id ?? null,
|
||||
parentRecordId: entry.parent_record_id ?? null,
|
||||
parentObject: entry.parent_object ?? null,
|
||||
createdAt: entry.created_at ?? null,
|
||||
entryValues: entry.entry_values ?? {},
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: LIST_ENTRY_OUTPUT_PROPERTIES,
|
||||
}
|
||||
64
apps/sim/tools/attio/get_member.ts
Normal file
64
apps/sim/tools/attio/get_member.ts
Normal file
@@ -0,0 +1,64 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
import type { AttioGetMemberParams, AttioGetMemberResponse } from './types'
|
||||
import { MEMBER_OUTPUT_PROPERTIES } from './types'
|
||||
|
||||
const logger = createLogger('AttioGetMember')
|
||||
|
||||
export const attioGetMemberTool: ToolConfig<AttioGetMemberParams, AttioGetMemberResponse> = {
|
||||
id: 'attio_get_member',
|
||||
name: 'Attio Get Member',
|
||||
description: 'Get a single workspace member by ID',
|
||||
version: '1.0.0',
|
||||
|
||||
oauth: {
|
||||
required: true,
|
||||
provider: 'attio',
|
||||
},
|
||||
|
||||
params: {
|
||||
accessToken: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'hidden',
|
||||
description: 'The OAuth access token for the Attio API',
|
||||
},
|
||||
memberId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'The workspace member ID',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => `https://api.attio.com/v2/workspace_members/${params.memberId}`,
|
||||
method: 'GET',
|
||||
headers: (params) => ({
|
||||
Authorization: `Bearer ${params.accessToken}`,
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response) => {
|
||||
const data = await response.json()
|
||||
if (!response.ok) {
|
||||
logger.error('Attio API request failed', { data, status: response.status })
|
||||
throw new Error(data.message || 'Failed to get workspace member')
|
||||
}
|
||||
const m = data.data
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
memberId: m.id?.workspace_member_id ?? null,
|
||||
firstName: m.first_name ?? null,
|
||||
lastName: m.last_name ?? null,
|
||||
avatarUrl: m.avatar_url ?? null,
|
||||
emailAddress: m.email_address ?? null,
|
||||
accessLevel: m.access_level ?? null,
|
||||
createdAt: m.created_at ?? null,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: MEMBER_OUTPUT_PROPERTIES,
|
||||
}
|
||||
67
apps/sim/tools/attio/get_note.ts
Normal file
67
apps/sim/tools/attio/get_note.ts
Normal file
@@ -0,0 +1,67 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
import type { AttioGetNoteParams, AttioGetNoteResponse } from './types'
|
||||
import { NOTE_OUTPUT_PROPERTIES } from './types'
|
||||
|
||||
const logger = createLogger('AttioGetNote')
|
||||
|
||||
export const attioGetNoteTool: ToolConfig<AttioGetNoteParams, AttioGetNoteResponse> = {
|
||||
id: 'attio_get_note',
|
||||
name: 'Attio Get Note',
|
||||
description: 'Get a single note by ID from Attio',
|
||||
version: '1.0.0',
|
||||
|
||||
oauth: {
|
||||
required: true,
|
||||
provider: 'attio',
|
||||
},
|
||||
|
||||
params: {
|
||||
accessToken: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'hidden',
|
||||
description: 'The OAuth access token for the Attio API',
|
||||
},
|
||||
noteId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'The ID of the note to retrieve',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => `https://api.attio.com/v2/notes/${params.noteId}`,
|
||||
method: 'GET',
|
||||
headers: (params) => ({
|
||||
Authorization: `Bearer ${params.accessToken}`,
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response) => {
|
||||
const data = await response.json()
|
||||
if (!response.ok) {
|
||||
logger.error('Attio API request failed', { data, status: response.status })
|
||||
throw new Error(data.message || 'Failed to get note')
|
||||
}
|
||||
const note = data.data
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
noteId: note.id?.note_id ?? null,
|
||||
parentObject: note.parent_object ?? null,
|
||||
parentRecordId: note.parent_record_id ?? null,
|
||||
title: note.title ?? null,
|
||||
contentPlaintext: note.content_plaintext ?? null,
|
||||
contentMarkdown: note.content_markdown ?? null,
|
||||
meetingId: note.meeting_id ?? null,
|
||||
tags: note.tags ?? [],
|
||||
createdByActor: note.created_by_actor ?? null,
|
||||
createdAt: note.created_at ?? null,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: NOTE_OUTPUT_PROPERTIES,
|
||||
}
|
||||
62
apps/sim/tools/attio/get_object.ts
Normal file
62
apps/sim/tools/attio/get_object.ts
Normal file
@@ -0,0 +1,62 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
import type { AttioGetObjectParams, AttioGetObjectResponse } from './types'
|
||||
import { OBJECT_OUTPUT_PROPERTIES } from './types'
|
||||
|
||||
const logger = createLogger('AttioGetObject')
|
||||
|
||||
export const attioGetObjectTool: ToolConfig<AttioGetObjectParams, AttioGetObjectResponse> = {
|
||||
id: 'attio_get_object',
|
||||
name: 'Attio Get Object',
|
||||
description: 'Get a single object by ID or slug',
|
||||
version: '1.0.0',
|
||||
|
||||
oauth: {
|
||||
required: true,
|
||||
provider: 'attio',
|
||||
},
|
||||
|
||||
params: {
|
||||
accessToken: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'hidden',
|
||||
description: 'The OAuth access token for the Attio API',
|
||||
},
|
||||
object: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'The object ID or slug (e.g. people, companies)',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => `https://api.attio.com/v2/objects/${params.object}`,
|
||||
method: 'GET',
|
||||
headers: (params) => ({
|
||||
Authorization: `Bearer ${params.accessToken}`,
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response) => {
|
||||
const data = await response.json()
|
||||
if (!response.ok) {
|
||||
logger.error('Attio API request failed', { data, status: response.status })
|
||||
throw new Error(data.message || 'Failed to get object')
|
||||
}
|
||||
const obj = data.data
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
objectId: obj.id?.object_id ?? null,
|
||||
apiSlug: obj.api_slug ?? null,
|
||||
singularNoun: obj.singular_noun ?? null,
|
||||
pluralNoun: obj.plural_noun ?? null,
|
||||
createdAt: obj.created_at ?? null,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: OBJECT_OUTPUT_PROPERTIES,
|
||||
}
|
||||
71
apps/sim/tools/attio/get_record.ts
Normal file
71
apps/sim/tools/attio/get_record.ts
Normal file
@@ -0,0 +1,71 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
import type { AttioGetRecordParams, AttioGetRecordResponse } from './types'
|
||||
import { RECORD_OBJECT_OUTPUT } from './types'
|
||||
|
||||
const logger = createLogger('AttioGetRecord')
|
||||
|
||||
export const attioGetRecordTool: ToolConfig<AttioGetRecordParams, AttioGetRecordResponse> = {
|
||||
id: 'attio_get_record',
|
||||
name: 'Attio Get Record',
|
||||
description: 'Get a single record by ID from Attio',
|
||||
version: '1.0.0',
|
||||
|
||||
oauth: {
|
||||
required: true,
|
||||
provider: 'attio',
|
||||
},
|
||||
|
||||
params: {
|
||||
accessToken: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'hidden',
|
||||
description: 'The OAuth access token for the Attio API',
|
||||
},
|
||||
objectType: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'The object type slug (e.g. people, companies)',
|
||||
},
|
||||
recordId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'The ID of the record to retrieve',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) =>
|
||||
`https://api.attio.com/v2/objects/${params.objectType}/records/${params.recordId}`,
|
||||
method: 'GET',
|
||||
headers: (params) => ({
|
||||
Authorization: `Bearer ${params.accessToken}`,
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response) => {
|
||||
const data = await response.json()
|
||||
if (!response.ok) {
|
||||
logger.error('Attio API request failed', { data, status: response.status })
|
||||
throw new Error(data.message || 'Failed to get record')
|
||||
}
|
||||
const record = data.data
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
record,
|
||||
recordId: record.id?.record_id ?? null,
|
||||
webUrl: record.web_url ?? null,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
record: RECORD_OBJECT_OUTPUT,
|
||||
recordId: { type: 'string', description: 'The record ID' },
|
||||
webUrl: { type: 'string', description: 'URL to view the record in Attio' },
|
||||
},
|
||||
}
|
||||
74
apps/sim/tools/attio/get_thread.ts
Normal file
74
apps/sim/tools/attio/get_thread.ts
Normal file
@@ -0,0 +1,74 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
import type { AttioGetThreadParams, AttioGetThreadResponse } from './types'
|
||||
import { THREAD_OUTPUT_PROPERTIES } from './types'
|
||||
|
||||
const logger = createLogger('AttioGetThread')
|
||||
|
||||
export const attioGetThreadTool: ToolConfig<AttioGetThreadParams, AttioGetThreadResponse> = {
|
||||
id: 'attio_get_thread',
|
||||
name: 'Attio Get Thread',
|
||||
description: 'Get a single comment thread by ID from Attio',
|
||||
version: '1.0.0',
|
||||
|
||||
oauth: {
|
||||
required: true,
|
||||
provider: 'attio',
|
||||
},
|
||||
|
||||
params: {
|
||||
accessToken: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'hidden',
|
||||
description: 'The OAuth access token for the Attio API',
|
||||
},
|
||||
threadId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'The thread ID',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => `https://api.attio.com/v2/threads/${params.threadId}`,
|
||||
method: 'GET',
|
||||
headers: (params) => ({
|
||||
Authorization: `Bearer ${params.accessToken}`,
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response) => {
|
||||
const data = await response.json()
|
||||
if (!response.ok) {
|
||||
logger.error('Attio API request failed', { data, status: response.status })
|
||||
throw new Error(data.message || 'Failed to get thread')
|
||||
}
|
||||
const t = data.data
|
||||
const comments =
|
||||
(
|
||||
t.comments as Array<{
|
||||
id?: { comment_id?: string }
|
||||
content_plaintext?: string
|
||||
author?: { type?: string; id?: string }
|
||||
created_at?: string
|
||||
}>
|
||||
)?.map((c) => ({
|
||||
commentId: c.id?.comment_id ?? null,
|
||||
contentPlaintext: c.content_plaintext ?? null,
|
||||
author: c.author ? { type: c.author.type ?? null, id: c.author.id ?? null } : null,
|
||||
createdAt: c.created_at ?? null,
|
||||
})) ?? []
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
threadId: t.id?.thread_id ?? null,
|
||||
comments,
|
||||
createdAt: t.created_at ?? null,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: THREAD_OUTPUT_PROPERTIES,
|
||||
}
|
||||
69
apps/sim/tools/attio/get_webhook.ts
Normal file
69
apps/sim/tools/attio/get_webhook.ts
Normal file
@@ -0,0 +1,69 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
import type { AttioGetWebhookParams, AttioGetWebhookResponse } from './types'
|
||||
import { WEBHOOK_OUTPUT_PROPERTIES } from './types'
|
||||
|
||||
const logger = createLogger('AttioGetWebhook')
|
||||
|
||||
export const attioGetWebhookTool: ToolConfig<AttioGetWebhookParams, AttioGetWebhookResponse> = {
|
||||
id: 'attio_get_webhook',
|
||||
name: 'Attio Get Webhook',
|
||||
description: 'Get a single webhook by ID from Attio',
|
||||
version: '1.0.0',
|
||||
|
||||
oauth: {
|
||||
required: true,
|
||||
provider: 'attio',
|
||||
},
|
||||
|
||||
params: {
|
||||
accessToken: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'hidden',
|
||||
description: 'The OAuth access token for the Attio API',
|
||||
},
|
||||
webhookId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'The webhook ID',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => `https://api.attio.com/v2/webhooks/${params.webhookId}`,
|
||||
method: 'GET',
|
||||
headers: (params) => ({
|
||||
Authorization: `Bearer ${params.accessToken}`,
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response) => {
|
||||
const data = await response.json()
|
||||
if (!response.ok) {
|
||||
logger.error('Attio API request failed', { data, status: response.status })
|
||||
throw new Error(data.message || 'Failed to get webhook')
|
||||
}
|
||||
const w = data.data
|
||||
const subs =
|
||||
(w.subscriptions as Array<{ event_type?: string; filter?: unknown }>)?.map(
|
||||
(s: { event_type?: string; filter?: unknown }) => ({
|
||||
eventType: s.event_type ?? null,
|
||||
filter: s.filter ?? null,
|
||||
})
|
||||
) ?? []
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
webhookId: w.id?.webhook_id ?? null,
|
||||
targetUrl: w.target_url ?? null,
|
||||
subscriptions: subs,
|
||||
status: w.status ?? null,
|
||||
createdAt: w.created_at ?? null,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: WEBHOOK_OUTPUT_PROPERTIES,
|
||||
}
|
||||
40
apps/sim/tools/attio/index.ts
Normal file
40
apps/sim/tools/attio/index.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
export { attioAssertRecordTool } from './assert_record'
|
||||
export { attioCreateCommentTool } from './create_comment'
|
||||
export { attioCreateListTool } from './create_list'
|
||||
export { attioCreateListEntryTool } from './create_list_entry'
|
||||
export { attioCreateNoteTool } from './create_note'
|
||||
export { attioCreateObjectTool } from './create_object'
|
||||
export { attioCreateRecordTool } from './create_record'
|
||||
export { attioCreateTaskTool } from './create_task'
|
||||
export { attioCreateWebhookTool } from './create_webhook'
|
||||
export { attioDeleteCommentTool } from './delete_comment'
|
||||
export { attioDeleteListEntryTool } from './delete_list_entry'
|
||||
export { attioDeleteNoteTool } from './delete_note'
|
||||
export { attioDeleteRecordTool } from './delete_record'
|
||||
export { attioDeleteTaskTool } from './delete_task'
|
||||
export { attioDeleteWebhookTool } from './delete_webhook'
|
||||
export { attioGetCommentTool } from './get_comment'
|
||||
export { attioGetListTool } from './get_list'
|
||||
export { attioGetListEntryTool } from './get_list_entry'
|
||||
export { attioGetMemberTool } from './get_member'
|
||||
export { attioGetNoteTool } from './get_note'
|
||||
export { attioGetObjectTool } from './get_object'
|
||||
export { attioGetRecordTool } from './get_record'
|
||||
export { attioGetThreadTool } from './get_thread'
|
||||
export { attioGetWebhookTool } from './get_webhook'
|
||||
export { attioListListsTool } from './list_lists'
|
||||
export { attioListMembersTool } from './list_members'
|
||||
export { attioListNotesTool } from './list_notes'
|
||||
export { attioListObjectsTool } from './list_objects'
|
||||
export { attioListRecordsTool } from './list_records'
|
||||
export { attioListTasksTool } from './list_tasks'
|
||||
export { attioListThreadsTool } from './list_threads'
|
||||
export { attioListWebhooksTool } from './list_webhooks'
|
||||
export { attioQueryListEntriesTool } from './query_list_entries'
|
||||
export { attioSearchRecordsTool } from './search_records'
|
||||
export { attioUpdateListTool } from './update_list'
|
||||
export { attioUpdateListEntryTool } from './update_list_entry'
|
||||
export { attioUpdateObjectTool } from './update_object'
|
||||
export { attioUpdateRecordTool } from './update_record'
|
||||
export { attioUpdateTaskTool } from './update_task'
|
||||
export { attioUpdateWebhookTool } from './update_webhook'
|
||||
78
apps/sim/tools/attio/list_lists.ts
Normal file
78
apps/sim/tools/attio/list_lists.ts
Normal file
@@ -0,0 +1,78 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
import type { AttioListListsParams, AttioListListsResponse } from './types'
|
||||
import { LIST_OUTPUT_PROPERTIES } from './types'
|
||||
|
||||
const logger = createLogger('AttioListLists')
|
||||
|
||||
export const attioListListsTool: ToolConfig<AttioListListsParams, AttioListListsResponse> = {
|
||||
id: 'attio_list_lists',
|
||||
name: 'Attio List Lists',
|
||||
description: 'List all lists in the Attio workspace',
|
||||
version: '1.0.0',
|
||||
|
||||
oauth: {
|
||||
required: true,
|
||||
provider: 'attio',
|
||||
},
|
||||
|
||||
params: {
|
||||
accessToken: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'hidden',
|
||||
description: 'The OAuth access token for the Attio API',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: 'https://api.attio.com/v2/lists',
|
||||
method: 'GET',
|
||||
headers: (params) => ({
|
||||
Authorization: `Bearer ${params.accessToken}`,
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response) => {
|
||||
const data = await response.json()
|
||||
if (!response.ok) {
|
||||
logger.error('Attio API request failed', { data, status: response.status })
|
||||
throw new Error(data.message || 'Failed to list lists')
|
||||
}
|
||||
const lists = (data.data ?? []).map((list: Record<string, unknown>) => {
|
||||
const id = list.id as { list_id?: string } | undefined
|
||||
const actor = list.created_by_actor as { type?: string; id?: string } | undefined
|
||||
return {
|
||||
listId: id?.list_id ?? null,
|
||||
apiSlug: (list.api_slug as string) ?? null,
|
||||
name: (list.name as string) ?? null,
|
||||
parentObject: Array.isArray(list.parent_object)
|
||||
? (list.parent_object[0] ?? null)
|
||||
: ((list.parent_object as string) ?? null),
|
||||
workspaceAccess: (list.workspace_access as string) ?? null,
|
||||
workspaceMemberAccess: list.workspace_member_access ?? null,
|
||||
createdByActor: actor ? { type: actor.type ?? null, id: actor.id ?? null } : null,
|
||||
createdAt: (list.created_at as string) ?? null,
|
||||
}
|
||||
})
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
lists,
|
||||
count: lists.length,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
lists: {
|
||||
type: 'array',
|
||||
description: 'Array of lists',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: LIST_OUTPUT_PROPERTIES,
|
||||
},
|
||||
},
|
||||
count: { type: 'number', description: 'Number of lists returned' },
|
||||
},
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user