mirror of
https://github.com/simstudioai/sim.git
synced 2026-04-06 03:00:16 -04:00
Compare commits
9 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e8f7fe0989 | ||
|
|
ace87791d8 | ||
|
|
74af452175 | ||
|
|
ec51f73596 | ||
|
|
6866da590c | ||
|
|
b0c0ee29a8 | ||
|
|
20c05644ab | ||
|
|
f9d73db65c | ||
|
|
e2e53aba76 |
@@ -1,6 +1,33 @@
|
||||
import type { SVGProps } from 'react'
|
||||
import { useId } from 'react'
|
||||
|
||||
export function AgentMailIcon(props: SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
<svg {...props} viewBox='0 0 350 363' fill='none' xmlns='http://www.w3.org/2000/svg'>
|
||||
<path
|
||||
d='M318.029 88.3407C196.474 115.33 153.48 115.321 33.9244 88.3271C30.6216 87.5814 27.1432 88.9727 25.3284 91.8313L1.24109 129.774C-1.76483 134.509 0.965276 140.798 6.46483 141.898C152.613 171.13 197.678 171.182 343.903 141.835C349.304 140.751 352.064 134.641 349.247 129.907L326.719 92.0479C324.95 89.0744 321.407 87.5907 318.029 88.3407Z'
|
||||
fill='currentColor'
|
||||
/>
|
||||
<path
|
||||
d='M75.9931 246.6L149.939 311.655C151.973 313.444 151.633 316.969 149.281 318.48L119.141 337.84C117.283 339.034 114.951 338.412 113.933 336.452L70.1276 252.036C68.0779 248.086 72.7553 243.751 75.9931 246.6Z'
|
||||
fill='currentColor'
|
||||
/>
|
||||
<path
|
||||
d='M274.025 246.6L200.08 311.655C198.046 313.444 198.385 316.969 200.737 318.48L230.877 337.84C232.736 339.034 235.068 338.412 236.085 336.452L279.891 252.036C281.941 248.086 277.263 243.751 274.025 246.6Z'
|
||||
fill='currentColor'
|
||||
/>
|
||||
<path
|
||||
d='M138.75 198.472L152.436 192.983C155.238 191.918 157.77 191.918 158.574 191.918C164.115 192.126 169.564 192.232 175.009 192.235C180.454 192.232 185.904 192.126 191.444 191.918C192.248 191.918 194.78 191.918 197.583 192.983L211.269 198.472C212.645 199.025 214.082 199.382 215.544 199.448C218.585 199.587 221.733 199.464 224.63 198.811C225.706 198.568 226.728 198.103 227.704 197.545L243.046 188.784C244.81 187.777 246.726 187.138 248.697 186.9L258.276 185.5H259.242H263.556L262.713 190.965L256.679 234.22C255.957 238.31 254.25 242.328 250.443 245.834L187.376 299.258C184.555 301.648 181.107 302.942 177.562 302.942H175.009H172.457C168.911 302.942 165.464 301.648 162.643 299.258L99.5761 245.834C95.7684 242.328 94.0614 238.31 93.3393 234.22L87.3059 190.965L86.4624 185.5H90.7771H91.7429L101.322 186.9C103.293 187.138 105.208 187.777 106.972 188.784L122.314 197.545C123.291 198.103 124.313 198.568 125.389 198.811C128.286 199.464 131.434 199.587 134.474 199.448C135.936 199.382 137.373 199.025 138.75 198.472Z'
|
||||
fill='currentColor'
|
||||
/>
|
||||
<path
|
||||
d='M102.47 0.847827C205.434 44.796 156.456 42.1015 248.434 1.63153C252.885 -1.09955 258.353 1.88915 259.419 7.69219L269.235 61.1686L270.819 69.7893L263.592 71.8231L263.582 71.8259C190.588 92.3069 165.244 92.0078 86.7576 71.7428L79.1971 69.7905L80.9925 60.8681L91.8401 6.91975C92.9559 1.3706 98.105 -1.55777 102.47 0.847827Z'
|
||||
fill='currentColor'
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
export function SearchIcon(props: SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
<svg
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
import type { ComponentType, SVGProps } from 'react'
|
||||
import {
|
||||
A2AIcon,
|
||||
AgentMailIcon,
|
||||
AhrefsIcon,
|
||||
AirtableIcon,
|
||||
AirweaveIcon,
|
||||
@@ -189,6 +190,7 @@ type IconComponent = ComponentType<SVGProps<SVGSVGElement>>
|
||||
|
||||
export const blockTypeToIconMap: Record<string, IconComponent> = {
|
||||
a2a: A2AIcon,
|
||||
agentmail: AgentMailIcon,
|
||||
ahrefs: AhrefsIcon,
|
||||
airtable: AirtableIcon,
|
||||
airweave: AirweaveIcon,
|
||||
|
||||
150
apps/docs/content/docs/en/blocks/credential.mdx
Normal file
150
apps/docs/content/docs/en/blocks/credential.mdx
Normal file
@@ -0,0 +1,150 @@
|
||||
---
|
||||
title: Credential
|
||||
---
|
||||
|
||||
import { Callout } from 'fumadocs-ui/components/callout'
|
||||
import { Tab, Tabs } from 'fumadocs-ui/components/tabs'
|
||||
import { Image } from '@/components/ui/image'
|
||||
import { FAQ } from '@/components/ui/faq'
|
||||
|
||||
The Credential block has two operations: **Select Credential** picks a single OAuth credential and outputs its ID reference for downstream blocks; **List Credentials** returns all OAuth credentials in the workspace (optionally filtered by provider) as an array for iteration.
|
||||
|
||||
<div className="flex justify-center">
|
||||
<Image
|
||||
src="/static/blocks/credential.png"
|
||||
alt="Credential Block"
|
||||
width={400}
|
||||
height={300}
|
||||
className="my-6"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Callout>
|
||||
The Credential block outputs credential **ID references**, not secrets. Downstream blocks receive the ID and resolve the actual OAuth token securely during their own execution.
|
||||
</Callout>
|
||||
|
||||
## Configuration Options
|
||||
|
||||
### Operation
|
||||
|
||||
| Value | Description |
|
||||
|---|---|
|
||||
| **Select Credential** | Pick one OAuth credential and output its reference — use this to wire a single credential into downstream blocks |
|
||||
| **List Credentials** | Return all OAuth credentials in the workspace as an array — use this with a ForEach loop |
|
||||
|
||||
### Credential (Select operation)
|
||||
|
||||
Select an OAuth credential from your workspace. The dropdown shows all connected OAuth accounts (Google, GitHub, Slack, etc.).
|
||||
|
||||
In advanced mode, paste a credential ID directly. You can copy a credential ID from your workspace's Credentials settings page.
|
||||
|
||||
### Provider (List operation)
|
||||
|
||||
Filter the returned OAuth credentials by provider. Select one or more providers from the dropdown — only providers you have credentials for will appear. Leave empty to return all OAuth credentials.
|
||||
|
||||
| Example | Returns |
|
||||
|---|---|
|
||||
| Gmail | Gmail credentials only |
|
||||
| Slack | Slack credentials only |
|
||||
| Gmail + Slack | Gmail and Slack credentials |
|
||||
|
||||
## Outputs
|
||||
|
||||
<Tabs items={['Select Credential', 'List Credentials']}>
|
||||
<Tab>
|
||||
| Output | Type | Description |
|
||||
|---|---|---|
|
||||
| `credentialId` | `string` | The credential ID — pipe this into other blocks' credential fields |
|
||||
| `displayName` | `string` | Human-readable name (e.g. "waleed@company.com") |
|
||||
| `providerId` | `string` | OAuth provider ID (e.g. `google-email`, `slack`) |
|
||||
</Tab>
|
||||
<Tab>
|
||||
| Output | Type | Description |
|
||||
|---|---|---|
|
||||
| `credentials` | `json` | Array of OAuth credential objects (see shape below) |
|
||||
| `count` | `number` | Number of credentials returned |
|
||||
|
||||
Each object in the `credentials` array:
|
||||
|
||||
| Field | Type | Description |
|
||||
|---|---|---|
|
||||
| `credentialId` | `string` | The credential ID |
|
||||
| `displayName` | `string` | Human-readable name |
|
||||
| `providerId` | `string` | OAuth provider ID |
|
||||
</Tab>
|
||||
</Tabs>
|
||||
|
||||
## Example Use Cases
|
||||
|
||||
**Shared credential across multiple blocks** — Define once, use everywhere
|
||||
```
|
||||
Credential (Select, Google) → Gmail (Send) & Google Drive (Upload) & Google Calendar (Create)
|
||||
```
|
||||
|
||||
**Multi-account workflows** — Route to different credentials based on logic
|
||||
```
|
||||
Agent (Determine account) → Condition → Credential A or Credential B → Slack (Post)
|
||||
```
|
||||
|
||||
**Iterate over all Gmail accounts**
|
||||
```
|
||||
Credential (List, Provider: Gmail) → ForEach Loop → Gmail (Send) using <loop.currentItem.credentialId>
|
||||
```
|
||||
|
||||
<div className="flex justify-center">
|
||||
<Image
|
||||
src="/static/blocks/credential-loop.png"
|
||||
alt="Credential List wired into a ForEach Loop"
|
||||
width={900}
|
||||
height={400}
|
||||
className="my-6"
|
||||
/>
|
||||
</div>
|
||||
|
||||
## How to wire a Credential block
|
||||
|
||||
### Select Credential
|
||||
|
||||
1. Drop a **Credential** block and select your OAuth credential from the picker
|
||||
2. In the downstream block, switch to **advanced mode** on its credential field
|
||||
3. Enter `<credentialBlockName.credentialId>` as the value
|
||||
|
||||
<Tabs items={['Gmail', 'Slack']}>
|
||||
<Tab>
|
||||
In the Gmail block's credential field (advanced mode):
|
||||
```
|
||||
<myCredential.credentialId>
|
||||
```
|
||||
</Tab>
|
||||
<Tab>
|
||||
In the Slack block's credential field (advanced mode):
|
||||
```
|
||||
<myCredential.credentialId>
|
||||
```
|
||||
</Tab>
|
||||
</Tabs>
|
||||
|
||||
### List Credentials
|
||||
|
||||
1. Drop a **Credential** block, set Operation to **List Credentials**
|
||||
2. Optionally select one or more **Providers** to narrow results (only your connected providers appear)
|
||||
3. Wire `<credentialBlockName.credentials>` into a **ForEach Loop** as the items source
|
||||
4. Inside the loop, reference `<loop.currentItem.credentialId>` in downstream blocks' credential fields
|
||||
|
||||
## Best Practices
|
||||
|
||||
- **Define once, reference many times**: When five blocks use the same Google account, use one Credential block and wire all five to `<credential.credentialId>` instead of selecting the account five times
|
||||
- **Outputs are safe to log**: The `credentialId` output is a UUID reference, not a secret. It is safe to inspect in execution logs
|
||||
- **Use for environment switching**: Pair with a Condition block to route to a production or staging OAuth credential based on a workflow variable
|
||||
- **Advanced mode is required**: Downstream blocks must be in advanced mode on their credential field to accept a dynamic reference
|
||||
- **Use List + ForEach for fan-out**: When you need to run the same action across all accounts of a provider, List Credentials feeds naturally into a ForEach loop
|
||||
- **Narrow by provider**: Use the Provider multiselect to filter to specific services — only providers you have credentials for are shown
|
||||
|
||||
<FAQ items={[
|
||||
{ question: "Does the Credential block expose my secret or token?", answer: "No. The block outputs a credential ID (a UUID), not the actual OAuth token. Downstream blocks receive the ID and resolve the token securely in their own execution context. Secrets never appear in workflow state, logs, or the canvas." },
|
||||
{ question: "What credential types does it support?", answer: "OAuth connected accounts only (Google, GitHub, Slack, etc.). Environment variables and service accounts cannot be resolved by ID in downstream blocks, so they are not supported." },
|
||||
{ question: "How is Select different from just copying a credential ID into advanced mode?", answer: "Functionally identical — both pass the same credential ID to the downstream block. The Credential block adds value when you need to use one credential in many blocks (change it once), or when you want to select between credentials dynamically using a Condition block." },
|
||||
{ question: "Can I list all OAuth credentials in my workspace?", answer: "Yes. Set the Operation to 'List Credentials'. Optionally filter by provider using the Provider multiselect. Wire the credentials output into a ForEach loop to process each credential individually." },
|
||||
{ question: "Can I use a Credential block output in a Function block?", answer: "Yes. Reference <credential.credentialId> in your Function block's code. Note that the function will receive the raw UUID string — if you need the resolved token, the downstream block must handle the resolution (as integration blocks do). The Function block does not automatically resolve credential IDs." },
|
||||
{ question: "What happens if the credential is deleted?", answer: "The Select operation will throw an error at execution time: 'Credential not found'. The List operation will simply omit the deleted credential from the results. Update the Credential block to select a valid credential before re-running." },
|
||||
]} />
|
||||
@@ -4,6 +4,7 @@
|
||||
"agent",
|
||||
"api",
|
||||
"condition",
|
||||
"credential",
|
||||
"evaluator",
|
||||
"function",
|
||||
"guardrails",
|
||||
|
||||
592
apps/docs/content/docs/en/tools/agentmail.mdx
Normal file
592
apps/docs/content/docs/en/tools/agentmail.mdx
Normal file
@@ -0,0 +1,592 @@
|
||||
---
|
||||
title: AgentMail
|
||||
description: Manage email inboxes, threads, and messages with AgentMail
|
||||
---
|
||||
|
||||
import { BlockInfoCard } from "@/components/ui/block-info-card"
|
||||
|
||||
<BlockInfoCard
|
||||
type="agentmail"
|
||||
color="#000000"
|
||||
/>
|
||||
|
||||
{/* MANUAL-CONTENT-START:intro */}
|
||||
[AgentMail](https://agentmail.to/) is an API-first email platform built for agents and automation. AgentMail lets you create email inboxes on the fly, send and receive messages, reply to threads, manage drafts, and organize conversations with labels — all through a simple REST API designed for programmatic access.
|
||||
|
||||
**Why AgentMail?**
|
||||
- **Agent-Native Email:** Purpose-built for AI agents and automation — create inboxes, send messages, and manage threads without human-facing UI overhead.
|
||||
- **Full Email Lifecycle:** Send new messages, reply to threads, forward emails, manage drafts, and schedule sends — all from a single API.
|
||||
- **Thread & Conversation Management:** Organize emails into threads with full read, reply, forward, and label support for structured conversation tracking.
|
||||
- **Draft Workflow:** Compose drafts, update them, schedule sends, and dispatch when ready — perfect for review-before-send workflows.
|
||||
- **Label Organization:** Tag threads and messages with custom labels for filtering, routing, and downstream automation.
|
||||
|
||||
**Using AgentMail in Sim**
|
||||
|
||||
Sim's AgentMail integration connects your agentic workflows directly to AgentMail using an API key. With 20 operations spanning inboxes, threads, messages, and drafts, you can build powerful email automations without writing backend code.
|
||||
|
||||
**Key benefits of using AgentMail in Sim:**
|
||||
- **Dynamic inbox creation:** Spin up new inboxes on the fly for each agent, workflow, or customer — perfect for multi-tenant email handling.
|
||||
- **Automated email processing:** List and read incoming messages, then trigger downstream actions based on content, sender, or labels.
|
||||
- **Conversational email:** Reply to threads and forward messages to keep conversations flowing naturally within your automated workflows.
|
||||
- **Draft and review workflows:** Create drafts, update them with AI-generated content, and send when approved — ideal for human-in-the-loop patterns.
|
||||
- **Email organization:** Apply labels to threads and messages to categorize, filter, and route emails through your automation pipeline.
|
||||
|
||||
Whether you're building an AI email assistant, automating customer support replies, processing incoming leads, or managing multi-agent email workflows, AgentMail in Sim gives you direct, secure access to the full AgentMail API — no middleware required. Simply configure your API key, select the operation you need, and let Sim handle the rest.
|
||||
{/* MANUAL-CONTENT-END */}
|
||||
|
||||
|
||||
## Usage Instructions
|
||||
|
||||
Integrate AgentMail into your workflow. Create and manage email inboxes, send and receive messages, reply to threads, manage drafts, and organize threads with labels. Requires API Key.
|
||||
|
||||
|
||||
|
||||
## Tools
|
||||
|
||||
### `agentmail_create_draft`
|
||||
|
||||
Create a new email draft in AgentMail
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | AgentMail API key |
|
||||
| `inboxId` | string | Yes | ID of the inbox to create the draft in |
|
||||
| `to` | string | No | Recipient email addresses \(comma-separated\) |
|
||||
| `subject` | string | No | Draft subject line |
|
||||
| `text` | string | No | Plain text draft body |
|
||||
| `html` | string | No | HTML draft body |
|
||||
| `cc` | string | No | CC recipient email addresses \(comma-separated\) |
|
||||
| `bcc` | string | No | BCC recipient email addresses \(comma-separated\) |
|
||||
| `inReplyTo` | string | No | ID of message being replied to |
|
||||
| `sendAt` | string | No | ISO 8601 timestamp to schedule sending |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `draftId` | string | Unique identifier for the draft |
|
||||
| `inboxId` | string | Inbox the draft belongs to |
|
||||
| `subject` | string | Draft subject |
|
||||
| `to` | array | Recipient email addresses |
|
||||
| `cc` | array | CC email addresses |
|
||||
| `bcc` | array | BCC email addresses |
|
||||
| `text` | string | Plain text content |
|
||||
| `html` | string | HTML content |
|
||||
| `preview` | string | Draft preview text |
|
||||
| `labels` | array | Labels assigned to the draft |
|
||||
| `inReplyTo` | string | Message ID this draft replies to |
|
||||
| `sendStatus` | string | Send status \(scheduled, sending, failed\) |
|
||||
| `sendAt` | string | Scheduled send time |
|
||||
| `createdAt` | string | Creation timestamp |
|
||||
| `updatedAt` | string | Last updated timestamp |
|
||||
|
||||
### `agentmail_create_inbox`
|
||||
|
||||
Create a new email inbox with AgentMail
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | AgentMail API key |
|
||||
| `username` | string | No | Username for the inbox email address |
|
||||
| `domain` | string | No | Domain for the inbox email address |
|
||||
| `displayName` | string | No | Display name for the inbox |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `inboxId` | string | Unique identifier for the inbox |
|
||||
| `email` | string | Email address of the inbox |
|
||||
| `displayName` | string | Display name of the inbox |
|
||||
| `createdAt` | string | Creation timestamp |
|
||||
| `updatedAt` | string | Last updated timestamp |
|
||||
|
||||
### `agentmail_delete_draft`
|
||||
|
||||
Delete an email draft in AgentMail
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | AgentMail API key |
|
||||
| `inboxId` | string | Yes | ID of the inbox containing the draft |
|
||||
| `draftId` | string | Yes | ID of the draft to delete |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `deleted` | boolean | Whether the draft was successfully deleted |
|
||||
|
||||
### `agentmail_delete_inbox`
|
||||
|
||||
Delete an email inbox in AgentMail
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | AgentMail API key |
|
||||
| `inboxId` | string | Yes | ID of the inbox to delete |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `deleted` | boolean | Whether the inbox was successfully deleted |
|
||||
|
||||
### `agentmail_delete_thread`
|
||||
|
||||
Delete an email thread in AgentMail (moves to trash, or permanently deletes if already in trash)
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | AgentMail API key |
|
||||
| `inboxId` | string | Yes | ID of the inbox containing the thread |
|
||||
| `threadId` | string | Yes | ID of the thread to delete |
|
||||
| `permanent` | boolean | No | Force permanent deletion instead of moving to trash |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `deleted` | boolean | Whether the thread was successfully deleted |
|
||||
|
||||
### `agentmail_forward_message`
|
||||
|
||||
Forward an email message to new recipients in AgentMail
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | AgentMail API key |
|
||||
| `inboxId` | string | Yes | ID of the inbox containing the message |
|
||||
| `messageId` | string | Yes | ID of the message to forward |
|
||||
| `to` | string | Yes | Recipient email addresses \(comma-separated\) |
|
||||
| `subject` | string | No | Override subject line |
|
||||
| `text` | string | No | Additional plain text to prepend |
|
||||
| `html` | string | No | Additional HTML to prepend |
|
||||
| `cc` | string | No | CC recipient email addresses \(comma-separated\) |
|
||||
| `bcc` | string | No | BCC recipient email addresses \(comma-separated\) |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `messageId` | string | ID of the forwarded message |
|
||||
| `threadId` | string | ID of the thread |
|
||||
|
||||
### `agentmail_get_draft`
|
||||
|
||||
Get details of a specific email draft in AgentMail
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | AgentMail API key |
|
||||
| `inboxId` | string | Yes | ID of the inbox the draft belongs to |
|
||||
| `draftId` | string | Yes | ID of the draft to retrieve |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `draftId` | string | Unique identifier for the draft |
|
||||
| `inboxId` | string | Inbox the draft belongs to |
|
||||
| `subject` | string | Draft subject |
|
||||
| `to` | array | Recipient email addresses |
|
||||
| `cc` | array | CC email addresses |
|
||||
| `bcc` | array | BCC email addresses |
|
||||
| `text` | string | Plain text content |
|
||||
| `html` | string | HTML content |
|
||||
| `preview` | string | Draft preview text |
|
||||
| `labels` | array | Labels assigned to the draft |
|
||||
| `inReplyTo` | string | Message ID this draft replies to |
|
||||
| `sendStatus` | string | Send status \(scheduled, sending, failed\) |
|
||||
| `sendAt` | string | Scheduled send time |
|
||||
| `createdAt` | string | Creation timestamp |
|
||||
| `updatedAt` | string | Last updated timestamp |
|
||||
|
||||
### `agentmail_get_inbox`
|
||||
|
||||
Get details of a specific email inbox in AgentMail
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | AgentMail API key |
|
||||
| `inboxId` | string | Yes | ID of the inbox to retrieve |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `inboxId` | string | Unique identifier for the inbox |
|
||||
| `email` | string | Email address of the inbox |
|
||||
| `displayName` | string | Display name of the inbox |
|
||||
| `createdAt` | string | Creation timestamp |
|
||||
| `updatedAt` | string | Last updated timestamp |
|
||||
|
||||
### `agentmail_get_message`
|
||||
|
||||
Get details of a specific email message in AgentMail
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | AgentMail API key |
|
||||
| `inboxId` | string | Yes | ID of the inbox containing the message |
|
||||
| `messageId` | string | Yes | ID of the message to retrieve |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `messageId` | string | Unique identifier for the message |
|
||||
| `threadId` | string | ID of the thread this message belongs to |
|
||||
| `from` | string | Sender email address |
|
||||
| `to` | array | Recipient email addresses |
|
||||
| `cc` | array | CC email addresses |
|
||||
| `bcc` | array | BCC email addresses |
|
||||
| `subject` | string | Message subject |
|
||||
| `text` | string | Plain text content |
|
||||
| `html` | string | HTML content |
|
||||
| `createdAt` | string | Creation timestamp |
|
||||
|
||||
### `agentmail_get_thread`
|
||||
|
||||
Get details of a specific email thread including messages in AgentMail
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | AgentMail API key |
|
||||
| `inboxId` | string | Yes | ID of the inbox containing the thread |
|
||||
| `threadId` | string | Yes | ID of the thread to retrieve |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `threadId` | string | Unique identifier for the thread |
|
||||
| `subject` | string | Thread subject |
|
||||
| `senders` | array | List of sender email addresses |
|
||||
| `recipients` | array | List of recipient email addresses |
|
||||
| `messageCount` | number | Number of messages in the thread |
|
||||
| `labels` | array | Labels assigned to the thread |
|
||||
| `lastMessageAt` | string | Timestamp of last message |
|
||||
| `createdAt` | string | Creation timestamp |
|
||||
| `updatedAt` | string | Last updated timestamp |
|
||||
| `messages` | array | Messages in the thread |
|
||||
| ↳ `messageId` | string | Unique identifier for the message |
|
||||
| ↳ `from` | string | Sender email address |
|
||||
| ↳ `to` | array | Recipient email addresses |
|
||||
| ↳ `cc` | array | CC email addresses |
|
||||
| ↳ `bcc` | array | BCC email addresses |
|
||||
| ↳ `subject` | string | Message subject |
|
||||
| ↳ `text` | string | Plain text content |
|
||||
| ↳ `html` | string | HTML content |
|
||||
| ↳ `createdAt` | string | Creation timestamp |
|
||||
|
||||
### `agentmail_list_drafts`
|
||||
|
||||
List email drafts in an inbox in AgentMail
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | AgentMail API key |
|
||||
| `inboxId` | string | Yes | ID of the inbox to list drafts from |
|
||||
| `limit` | number | No | Maximum number of drafts to return |
|
||||
| `pageToken` | string | No | Pagination token for next page of results |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `drafts` | array | List of drafts |
|
||||
| ↳ `draftId` | string | Unique identifier for the draft |
|
||||
| ↳ `inboxId` | string | Inbox the draft belongs to |
|
||||
| ↳ `subject` | string | Draft subject |
|
||||
| ↳ `to` | array | Recipient email addresses |
|
||||
| ↳ `cc` | array | CC email addresses |
|
||||
| ↳ `bcc` | array | BCC email addresses |
|
||||
| ↳ `preview` | string | Draft preview text |
|
||||
| ↳ `sendStatus` | string | Send status \(scheduled, sending, failed\) |
|
||||
| ↳ `sendAt` | string | Scheduled send time |
|
||||
| ↳ `createdAt` | string | Creation timestamp |
|
||||
| ↳ `updatedAt` | string | Last updated timestamp |
|
||||
| `count` | number | Total number of drafts |
|
||||
| `nextPageToken` | string | Token for retrieving the next page |
|
||||
|
||||
### `agentmail_list_inboxes`
|
||||
|
||||
List all email inboxes in AgentMail
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | AgentMail API key |
|
||||
| `limit` | number | No | Maximum number of inboxes to return |
|
||||
| `pageToken` | string | No | Pagination token for next page of results |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `inboxes` | array | List of inboxes |
|
||||
| ↳ `inboxId` | string | Unique identifier for the inbox |
|
||||
| ↳ `email` | string | Email address of the inbox |
|
||||
| ↳ `displayName` | string | Display name of the inbox |
|
||||
| ↳ `createdAt` | string | Creation timestamp |
|
||||
| ↳ `updatedAt` | string | Last updated timestamp |
|
||||
| `count` | number | Total number of inboxes |
|
||||
| `nextPageToken` | string | Token for retrieving the next page |
|
||||
|
||||
### `agentmail_list_messages`
|
||||
|
||||
List messages in an inbox in AgentMail
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | AgentMail API key |
|
||||
| `inboxId` | string | Yes | ID of the inbox to list messages from |
|
||||
| `limit` | number | No | Maximum number of messages to return |
|
||||
| `pageToken` | string | No | Pagination token for next page of results |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `messages` | array | List of messages in the inbox |
|
||||
| ↳ `messageId` | string | Unique identifier for the message |
|
||||
| ↳ `from` | string | Sender email address |
|
||||
| ↳ `to` | array | Recipient email addresses |
|
||||
| ↳ `subject` | string | Message subject |
|
||||
| ↳ `preview` | string | Message preview text |
|
||||
| ↳ `createdAt` | string | Creation timestamp |
|
||||
| `count` | number | Total number of messages |
|
||||
| `nextPageToken` | string | Token for retrieving the next page |
|
||||
|
||||
### `agentmail_list_threads`
|
||||
|
||||
List email threads in AgentMail
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | AgentMail API key |
|
||||
| `inboxId` | string | Yes | ID of the inbox to list threads from |
|
||||
| `limit` | number | No | Maximum number of threads to return |
|
||||
| `pageToken` | string | No | Pagination token for next page of results |
|
||||
| `labels` | string | No | Comma-separated labels to filter threads by |
|
||||
| `before` | string | No | Filter threads before this ISO 8601 timestamp |
|
||||
| `after` | string | No | Filter threads after this ISO 8601 timestamp |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `threads` | array | List of email threads |
|
||||
| ↳ `threadId` | string | Unique identifier for the thread |
|
||||
| ↳ `subject` | string | Thread subject |
|
||||
| ↳ `senders` | array | List of sender email addresses |
|
||||
| ↳ `recipients` | array | List of recipient email addresses |
|
||||
| ↳ `messageCount` | number | Number of messages in the thread |
|
||||
| ↳ `lastMessageAt` | string | Timestamp of last message |
|
||||
| ↳ `createdAt` | string | Creation timestamp |
|
||||
| ↳ `updatedAt` | string | Last updated timestamp |
|
||||
| `count` | number | Total number of threads |
|
||||
| `nextPageToken` | string | Token for retrieving the next page |
|
||||
|
||||
### `agentmail_reply_message`
|
||||
|
||||
Reply to an existing email message in AgentMail
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | AgentMail API key |
|
||||
| `inboxId` | string | Yes | ID of the inbox to reply from |
|
||||
| `messageId` | string | Yes | ID of the message to reply to |
|
||||
| `text` | string | No | Plain text reply body |
|
||||
| `html` | string | No | HTML reply body |
|
||||
| `to` | string | No | Override recipient email addresses \(comma-separated\) |
|
||||
| `cc` | string | No | CC email addresses \(comma-separated\) |
|
||||
| `bcc` | string | No | BCC email addresses \(comma-separated\) |
|
||||
| `replyAll` | boolean | No | Reply to all recipients of the original message |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `messageId` | string | ID of the sent reply message |
|
||||
| `threadId` | string | ID of the thread |
|
||||
|
||||
### `agentmail_send_draft`
|
||||
|
||||
Send an existing email draft in AgentMail
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | AgentMail API key |
|
||||
| `inboxId` | string | Yes | ID of the inbox containing the draft |
|
||||
| `draftId` | string | Yes | ID of the draft to send |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `messageId` | string | ID of the sent message |
|
||||
| `threadId` | string | ID of the thread |
|
||||
|
||||
### `agentmail_send_message`
|
||||
|
||||
Send an email message from an AgentMail inbox
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | AgentMail API key |
|
||||
| `inboxId` | string | Yes | ID of the inbox to send from |
|
||||
| `to` | string | Yes | Recipient email address \(comma-separated for multiple\) |
|
||||
| `subject` | string | Yes | Email subject line |
|
||||
| `text` | string | No | Plain text email body |
|
||||
| `html` | string | No | HTML email body |
|
||||
| `cc` | string | No | CC recipient email addresses \(comma-separated\) |
|
||||
| `bcc` | string | No | BCC recipient email addresses \(comma-separated\) |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `threadId` | string | ID of the created thread |
|
||||
| `messageId` | string | ID of the sent message |
|
||||
| `subject` | string | Email subject line |
|
||||
| `to` | string | Recipient email address |
|
||||
|
||||
### `agentmail_update_draft`
|
||||
|
||||
Update an existing email draft in AgentMail
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | AgentMail API key |
|
||||
| `inboxId` | string | Yes | ID of the inbox containing the draft |
|
||||
| `draftId` | string | Yes | ID of the draft to update |
|
||||
| `to` | string | No | Recipient email addresses \(comma-separated\) |
|
||||
| `subject` | string | No | Draft subject line |
|
||||
| `text` | string | No | Plain text draft body |
|
||||
| `html` | string | No | HTML draft body |
|
||||
| `cc` | string | No | CC recipient email addresses \(comma-separated\) |
|
||||
| `bcc` | string | No | BCC recipient email addresses \(comma-separated\) |
|
||||
| `sendAt` | string | No | ISO 8601 timestamp to schedule sending |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `draftId` | string | Unique identifier for the draft |
|
||||
| `inboxId` | string | Inbox the draft belongs to |
|
||||
| `subject` | string | Draft subject |
|
||||
| `to` | array | Recipient email addresses |
|
||||
| `cc` | array | CC email addresses |
|
||||
| `bcc` | array | BCC email addresses |
|
||||
| `text` | string | Plain text content |
|
||||
| `html` | string | HTML content |
|
||||
| `preview` | string | Draft preview text |
|
||||
| `labels` | array | Labels assigned to the draft |
|
||||
| `inReplyTo` | string | Message ID this draft replies to |
|
||||
| `sendStatus` | string | Send status \(scheduled, sending, failed\) |
|
||||
| `sendAt` | string | Scheduled send time |
|
||||
| `createdAt` | string | Creation timestamp |
|
||||
| `updatedAt` | string | Last updated timestamp |
|
||||
|
||||
### `agentmail_update_inbox`
|
||||
|
||||
Update the display name of an email inbox in AgentMail
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | AgentMail API key |
|
||||
| `inboxId` | string | Yes | ID of the inbox to update |
|
||||
| `displayName` | string | Yes | New display name for the inbox |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `inboxId` | string | Unique identifier for the inbox |
|
||||
| `email` | string | Email address of the inbox |
|
||||
| `displayName` | string | Display name of the inbox |
|
||||
| `createdAt` | string | Creation timestamp |
|
||||
| `updatedAt` | string | Last updated timestamp |
|
||||
|
||||
### `agentmail_update_message`
|
||||
|
||||
Add or remove labels on an email message in AgentMail
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | AgentMail API key |
|
||||
| `inboxId` | string | Yes | ID of the inbox containing the message |
|
||||
| `messageId` | string | Yes | ID of the message to update |
|
||||
| `addLabels` | string | No | Comma-separated labels to add to the message |
|
||||
| `removeLabels` | string | No | Comma-separated labels to remove from the message |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `messageId` | string | Unique identifier for the message |
|
||||
| `labels` | array | Current labels on the message |
|
||||
|
||||
### `agentmail_update_thread`
|
||||
|
||||
Add or remove labels on an email thread in AgentMail
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | AgentMail API key |
|
||||
| `inboxId` | string | Yes | ID of the inbox containing the thread |
|
||||
| `threadId` | string | Yes | ID of the thread to update |
|
||||
| `addLabels` | string | No | Comma-separated labels to add to the thread |
|
||||
| `removeLabels` | string | No | Comma-separated labels to remove from the thread |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `threadId` | string | Unique identifier for the thread |
|
||||
| `labels` | array | Current labels on the thread |
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
"pages": [
|
||||
"index",
|
||||
"a2a",
|
||||
"agentmail",
|
||||
"ahrefs",
|
||||
"airtable",
|
||||
"airweave",
|
||||
|
||||
@@ -245,6 +245,7 @@ Create a new alert in Rootly for on-call notification and routing.
|
||||
| --------- | ---- | ----------- |
|
||||
| `alert` | object | The created alert |
|
||||
| ↳ `id` | string | Unique alert ID |
|
||||
| ↳ `shortId` | string | Short alert ID |
|
||||
| ↳ `summary` | string | Alert summary |
|
||||
| ↳ `description` | string | Alert description |
|
||||
| ↳ `source` | string | Alert source |
|
||||
@@ -254,6 +255,8 @@ Create a new alert in Rootly for on-call notification and routing.
|
||||
| ↳ `deduplicationKey` | string | Deduplication key |
|
||||
| ↳ `createdAt` | string | Creation date |
|
||||
| ↳ `updatedAt` | string | Last update date |
|
||||
| ↳ `startedAt` | string | Start date |
|
||||
| ↳ `endedAt` | string | End date |
|
||||
|
||||
### `rootly_list_alerts`
|
||||
|
||||
@@ -278,6 +281,7 @@ List alerts from Rootly with optional filtering by status, source, and services.
|
||||
| --------- | ---- | ----------- |
|
||||
| `alerts` | array | List of alerts |
|
||||
| ↳ `id` | string | Unique alert ID |
|
||||
| ↳ `shortId` | string | Short alert ID |
|
||||
| ↳ `summary` | string | Alert summary |
|
||||
| ↳ `description` | string | Alert description |
|
||||
| ↳ `source` | string | Alert source |
|
||||
@@ -287,6 +291,8 @@ List alerts from Rootly with optional filtering by status, source, and services.
|
||||
| ↳ `deduplicationKey` | string | Deduplication key |
|
||||
| ↳ `createdAt` | string | Creation date |
|
||||
| ↳ `updatedAt` | string | Last update date |
|
||||
| ↳ `startedAt` | string | Start date |
|
||||
| ↳ `endedAt` | string | End date |
|
||||
| `totalCount` | number | Total number of alerts returned |
|
||||
|
||||
### `rootly_add_incident_event`
|
||||
@@ -507,4 +513,379 @@ List incident retrospectives (post-mortems) from Rootly.
|
||||
| ↳ `updatedAt` | string | Last update date |
|
||||
| `totalCount` | number | Total number of retrospectives returned |
|
||||
|
||||
### `rootly_delete_incident`
|
||||
|
||||
Delete an incident by ID from Rootly.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | Rootly API key |
|
||||
| `incidentId` | string | Yes | The ID of the incident to delete |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `success` | boolean | Whether the deletion succeeded |
|
||||
| `message` | string | Result message |
|
||||
|
||||
### `rootly_get_alert`
|
||||
|
||||
Retrieve a single alert by ID from Rootly.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | Rootly API key |
|
||||
| `alertId` | string | Yes | The ID of the alert to retrieve |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `alert` | object | The alert details |
|
||||
| ↳ `id` | string | Unique alert ID |
|
||||
| ↳ `shortId` | string | Short alert ID |
|
||||
| ↳ `summary` | string | Alert summary |
|
||||
| ↳ `description` | string | Alert description |
|
||||
| ↳ `source` | string | Alert source |
|
||||
| ↳ `status` | string | Alert status |
|
||||
| ↳ `externalId` | string | External ID |
|
||||
| ↳ `externalUrl` | string | External URL |
|
||||
| ↳ `deduplicationKey` | string | Deduplication key |
|
||||
| ↳ `createdAt` | string | Creation date |
|
||||
| ↳ `updatedAt` | string | Last update date |
|
||||
| ↳ `startedAt` | string | Start date |
|
||||
| ↳ `endedAt` | string | End date |
|
||||
|
||||
### `rootly_update_alert`
|
||||
|
||||
Update an existing alert in Rootly.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | Rootly API key |
|
||||
| `alertId` | string | Yes | The ID of the alert to update |
|
||||
| `summary` | string | No | Updated alert summary |
|
||||
| `description` | string | No | Updated alert description |
|
||||
| `source` | string | No | Updated alert source |
|
||||
| `serviceIds` | string | No | Comma-separated service IDs to attach |
|
||||
| `groupIds` | string | No | Comma-separated team/group IDs to attach |
|
||||
| `environmentIds` | string | No | Comma-separated environment IDs to attach |
|
||||
| `externalId` | string | No | Updated external ID |
|
||||
| `externalUrl` | string | No | Updated external URL |
|
||||
| `deduplicationKey` | string | No | Updated deduplication key |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `alert` | object | The updated alert |
|
||||
| ↳ `id` | string | Unique alert ID |
|
||||
| ↳ `shortId` | string | Short alert ID |
|
||||
| ↳ `summary` | string | Alert summary |
|
||||
| ↳ `description` | string | Alert description |
|
||||
| ↳ `source` | string | Alert source |
|
||||
| ↳ `status` | string | Alert status |
|
||||
| ↳ `externalId` | string | External ID |
|
||||
| ↳ `externalUrl` | string | External URL |
|
||||
| ↳ `deduplicationKey` | string | Deduplication key |
|
||||
| ↳ `createdAt` | string | Creation date |
|
||||
| ↳ `updatedAt` | string | Last update date |
|
||||
| ↳ `startedAt` | string | Start date |
|
||||
| ↳ `endedAt` | string | End date |
|
||||
|
||||
### `rootly_acknowledge_alert`
|
||||
|
||||
Acknowledge an alert in Rootly.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | Rootly API key |
|
||||
| `alertId` | string | Yes | The ID of the alert to acknowledge |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `alert` | object | The acknowledged alert |
|
||||
| ↳ `id` | string | Unique alert ID |
|
||||
| ↳ `shortId` | string | Short alert ID |
|
||||
| ↳ `summary` | string | Alert summary |
|
||||
| ↳ `description` | string | Alert description |
|
||||
| ↳ `source` | string | Alert source |
|
||||
| ↳ `status` | string | Alert status |
|
||||
| ↳ `externalId` | string | External ID |
|
||||
| ↳ `externalUrl` | string | External URL |
|
||||
| ↳ `deduplicationKey` | string | Deduplication key |
|
||||
| ↳ `createdAt` | string | Creation date |
|
||||
| ↳ `updatedAt` | string | Last update date |
|
||||
| ↳ `startedAt` | string | Start date |
|
||||
| ↳ `endedAt` | string | End date |
|
||||
|
||||
### `rootly_resolve_alert`
|
||||
|
||||
Resolve an alert in Rootly.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | Rootly API key |
|
||||
| `alertId` | string | Yes | The ID of the alert to resolve |
|
||||
| `resolutionMessage` | string | No | Message describing how the alert was resolved |
|
||||
| `resolveRelatedIncidents` | boolean | No | Whether to also resolve related incidents |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `alert` | object | The resolved alert |
|
||||
| ↳ `id` | string | Unique alert ID |
|
||||
| ↳ `shortId` | string | Short alert ID |
|
||||
| ↳ `summary` | string | Alert summary |
|
||||
| ↳ `description` | string | Alert description |
|
||||
| ↳ `source` | string | Alert source |
|
||||
| ↳ `status` | string | Alert status |
|
||||
| ↳ `externalId` | string | External ID |
|
||||
| ↳ `externalUrl` | string | External URL |
|
||||
| ↳ `deduplicationKey` | string | Deduplication key |
|
||||
| ↳ `createdAt` | string | Creation date |
|
||||
| ↳ `updatedAt` | string | Last update date |
|
||||
| ↳ `startedAt` | string | Start date |
|
||||
| ↳ `endedAt` | string | End date |
|
||||
|
||||
### `rootly_create_action_item`
|
||||
|
||||
Create a new action item for an incident in Rootly.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | Rootly API key |
|
||||
| `incidentId` | string | Yes | The ID of the incident to add the action item to |
|
||||
| `summary` | string | Yes | The title of the action item |
|
||||
| `description` | string | No | A detailed description of the action item |
|
||||
| `kind` | string | No | The kind of action item \(task, follow_up\) |
|
||||
| `priority` | string | No | Priority level \(high, medium, low\) |
|
||||
| `status` | string | No | Action item status \(open, in_progress, cancelled, done\) |
|
||||
| `assignedToUserId` | string | No | The user ID to assign the action item to |
|
||||
| `dueDate` | string | No | Due date for the action item |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `actionItem` | object | The created action item |
|
||||
| ↳ `id` | string | Unique action item ID |
|
||||
| ↳ `summary` | string | Action item title |
|
||||
| ↳ `description` | string | Action item description |
|
||||
| ↳ `kind` | string | Action item kind \(task, follow_up\) |
|
||||
| ↳ `priority` | string | Priority level |
|
||||
| ↳ `status` | string | Action item status |
|
||||
| ↳ `dueDate` | string | Due date |
|
||||
| ↳ `createdAt` | string | Creation date |
|
||||
| ↳ `updatedAt` | string | Last update date |
|
||||
|
||||
### `rootly_list_action_items`
|
||||
|
||||
List action items for an incident in Rootly.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | Rootly API key |
|
||||
| `incidentId` | string | Yes | The ID of the incident to list action items for |
|
||||
| `pageSize` | number | No | Number of items per page \(default: 20\) |
|
||||
| `pageNumber` | number | No | Page number for pagination |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `actionItems` | array | List of action items |
|
||||
| ↳ `id` | string | Unique action item ID |
|
||||
| ↳ `summary` | string | Action item title |
|
||||
| ↳ `description` | string | Action item description |
|
||||
| ↳ `kind` | string | Action item kind \(task, follow_up\) |
|
||||
| ↳ `priority` | string | Priority level |
|
||||
| ↳ `status` | string | Action item status |
|
||||
| ↳ `dueDate` | string | Due date |
|
||||
| ↳ `createdAt` | string | Creation date |
|
||||
| ↳ `updatedAt` | string | Last update date |
|
||||
| `totalCount` | number | Total number of action items returned |
|
||||
|
||||
### `rootly_list_users`
|
||||
|
||||
List users from Rootly with optional search and email filtering.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | Rootly API key |
|
||||
| `search` | string | No | Search term to filter users |
|
||||
| `email` | string | No | Filter users by email address |
|
||||
| `pageSize` | number | No | Number of items per page \(default: 20\) |
|
||||
| `pageNumber` | number | No | Page number for pagination |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `users` | array | List of users |
|
||||
| ↳ `id` | string | Unique user ID |
|
||||
| ↳ `email` | string | User email address |
|
||||
| ↳ `firstName` | string | User first name |
|
||||
| ↳ `lastName` | string | User last name |
|
||||
| ↳ `fullName` | string | User full name |
|
||||
| ↳ `timeZone` | string | User time zone |
|
||||
| ↳ `createdAt` | string | Creation date |
|
||||
| ↳ `updatedAt` | string | Last update date |
|
||||
| `totalCount` | number | Total number of users returned |
|
||||
|
||||
### `rootly_list_on_calls`
|
||||
|
||||
List current on-call entries from Rootly with optional filtering.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | Rootly API key |
|
||||
| `scheduleIds` | string | No | Comma-separated schedule IDs to filter by |
|
||||
| `escalationPolicyIds` | string | No | Comma-separated escalation policy IDs to filter by |
|
||||
| `userIds` | string | No | Comma-separated user IDs to filter by |
|
||||
| `serviceIds` | string | No | Comma-separated service IDs to filter by |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `onCalls` | array | List of on-call entries |
|
||||
| ↳ `id` | string | Unique on-call entry ID |
|
||||
| ↳ `userId` | string | ID of the on-call user |
|
||||
| ↳ `userName` | string | Name of the on-call user |
|
||||
| ↳ `scheduleId` | string | ID of the associated schedule |
|
||||
| ↳ `scheduleName` | string | Name of the associated schedule |
|
||||
| ↳ `escalationPolicyId` | string | ID of the associated escalation policy |
|
||||
| ↳ `startTime` | string | On-call start time |
|
||||
| ↳ `endTime` | string | On-call end time |
|
||||
| `totalCount` | number | Total number of on-call entries returned |
|
||||
|
||||
### `rootly_list_schedules`
|
||||
|
||||
List on-call schedules from Rootly with optional search filtering.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | Rootly API key |
|
||||
| `search` | string | No | Search term to filter schedules |
|
||||
| `pageSize` | number | No | Number of items per page \(default: 20\) |
|
||||
| `pageNumber` | number | No | Page number for pagination |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `schedules` | array | List of schedules |
|
||||
| ↳ `id` | string | Unique schedule ID |
|
||||
| ↳ `name` | string | Schedule name |
|
||||
| ↳ `description` | string | Schedule description |
|
||||
| ↳ `allTimeCoverage` | boolean | Whether schedule provides 24/7 coverage |
|
||||
| ↳ `createdAt` | string | Creation date |
|
||||
| ↳ `updatedAt` | string | Last update date |
|
||||
| `totalCount` | number | Total number of schedules returned |
|
||||
|
||||
### `rootly_list_escalation_policies`
|
||||
|
||||
List escalation policies from Rootly with optional search filtering.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | Rootly API key |
|
||||
| `search` | string | No | Search term to filter escalation policies |
|
||||
| `pageSize` | number | No | Number of items per page \(default: 20\) |
|
||||
| `pageNumber` | number | No | Page number for pagination |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `escalationPolicies` | array | List of escalation policies |
|
||||
| ↳ `id` | string | Unique escalation policy ID |
|
||||
| ↳ `name` | string | Escalation policy name |
|
||||
| ↳ `description` | string | Escalation policy description |
|
||||
| ↳ `repeatCount` | number | Number of times to repeat escalation |
|
||||
| ↳ `groupIds` | array | Associated group IDs |
|
||||
| ↳ `serviceIds` | array | Associated service IDs |
|
||||
| ↳ `createdAt` | string | Creation date |
|
||||
| ↳ `updatedAt` | string | Last update date |
|
||||
| `totalCount` | number | Total number of escalation policies returned |
|
||||
|
||||
### `rootly_list_causes`
|
||||
|
||||
List causes from Rootly with optional search filtering.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | Rootly API key |
|
||||
| `search` | string | No | Search term to filter causes |
|
||||
| `pageSize` | number | No | Number of items per page \(default: 20\) |
|
||||
| `pageNumber` | number | No | Page number for pagination |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `causes` | array | List of causes |
|
||||
| ↳ `id` | string | Unique cause ID |
|
||||
| ↳ `name` | string | Cause name |
|
||||
| ↳ `slug` | string | Cause slug |
|
||||
| ↳ `description` | string | Cause description |
|
||||
| ↳ `position` | number | Cause position |
|
||||
| ↳ `createdAt` | string | Creation date |
|
||||
| ↳ `updatedAt` | string | Last update date |
|
||||
| `totalCount` | number | Total number of causes returned |
|
||||
|
||||
### `rootly_list_playbooks`
|
||||
|
||||
List playbooks from Rootly with pagination support.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | Rootly API key |
|
||||
| `pageSize` | number | No | Number of items per page \(default: 20\) |
|
||||
| `pageNumber` | number | No | Page number for pagination |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `playbooks` | array | List of playbooks |
|
||||
| ↳ `id` | string | Unique playbook ID |
|
||||
| ↳ `title` | string | Playbook title |
|
||||
| ↳ `summary` | string | Playbook summary |
|
||||
| ↳ `externalUrl` | string | External URL |
|
||||
| ↳ `createdAt` | string | Creation date |
|
||||
| ↳ `updatedAt` | string | Last update date |
|
||||
| `totalCount` | number | Total number of playbooks returned |
|
||||
|
||||
|
||||
|
||||
BIN
apps/docs/public/static/blocks/credential-loop.png
Normal file
BIN
apps/docs/public/static/blocks/credential-loop.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 63 KiB |
BIN
apps/docs/public/static/blocks/credential.png
Normal file
BIN
apps/docs/public/static/blocks/credential.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 12 KiB |
@@ -1,16 +1,18 @@
|
||||
'use client'
|
||||
|
||||
import { Suspense, useMemo, useRef, useState } from 'react'
|
||||
import { Suspense, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { Turnstile, type TurnstileInstance } from '@marsidev/react-turnstile'
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { Eye, EyeOff, Loader2 } from 'lucide-react'
|
||||
import Link from 'next/link'
|
||||
import { useRouter, useSearchParams } from 'next/navigation'
|
||||
import { usePostHog } from 'posthog-js/react'
|
||||
import { Input, Label } from '@/components/emcn'
|
||||
import { client, useSession } from '@/lib/auth/auth-client'
|
||||
import { getEnv, isFalsy, isTruthy } from '@/lib/core/config/env'
|
||||
import { cn } from '@/lib/core/utils/cn'
|
||||
import { quickValidateEmail } from '@/lib/messaging/email/validation'
|
||||
import { captureEvent } from '@/lib/posthog/client'
|
||||
import { AUTH_SUBMIT_BTN } from '@/app/(auth)/components/auth-button-classes'
|
||||
import { SocialLoginButtons } from '@/app/(auth)/components/social-login-buttons'
|
||||
import { SSOLoginButton } from '@/app/(auth)/components/sso-login-button'
|
||||
@@ -81,7 +83,12 @@ function SignupFormContent({
|
||||
const router = useRouter()
|
||||
const searchParams = useSearchParams()
|
||||
const { refetch: refetchSession } = useSession()
|
||||
const posthog = usePostHog()
|
||||
const [isLoading, setIsLoading] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
captureEvent(posthog, 'signup_page_viewed', {})
|
||||
}, [posthog])
|
||||
const [showPassword, setShowPassword] = useState(false)
|
||||
const [password, setPassword] = useState('')
|
||||
const [passwordErrors, setPasswordErrors] = useState<string[]>([])
|
||||
|
||||
@@ -166,14 +166,14 @@ export function AuditLogPreview() {
|
||||
const counterRef = useRef(ENTRY_TEMPLATES.length)
|
||||
const templateIndexRef = useRef(6 % ENTRY_TEMPLATES.length)
|
||||
|
||||
const now = Date.now()
|
||||
const [entries, setEntries] = useState<LogEntry[]>(() =>
|
||||
ENTRY_TEMPLATES.slice(0, 6).map((t, i) => ({
|
||||
const [entries, setEntries] = useState<LogEntry[]>(() => {
|
||||
const now = Date.now()
|
||||
return ENTRY_TEMPLATES.slice(0, 6).map((t, i) => ({
|
||||
...t,
|
||||
id: i,
|
||||
insertedAt: now - INITIAL_OFFSETS_MS[i],
|
||||
}))
|
||||
)
|
||||
})
|
||||
const [, tick] = useState(0)
|
||||
|
||||
useEffect(() => {
|
||||
@@ -208,10 +208,9 @@ export function AuditLogPreview() {
|
||||
exit={{ opacity: 0 }}
|
||||
transition={{
|
||||
layout: {
|
||||
type: 'spring',
|
||||
stiffness: 350,
|
||||
damping: 50,
|
||||
mass: 0.8,
|
||||
type: 'tween',
|
||||
duration: 0.32,
|
||||
ease: [0.25, 0.46, 0.45, 0.94],
|
||||
},
|
||||
y: { duration: 0.32, ease: [0.25, 0.46, 0.45, 0.94] },
|
||||
opacity: { duration: 0.25 },
|
||||
|
||||
15
apps/sim/app/(home)/landing-analytics.tsx
Normal file
15
apps/sim/app/(home)/landing-analytics.tsx
Normal file
@@ -0,0 +1,15 @@
|
||||
'use client'
|
||||
|
||||
import { useEffect } from 'react'
|
||||
import { usePostHog } from 'posthog-js/react'
|
||||
import { captureEvent } from '@/lib/posthog/client'
|
||||
|
||||
export function LandingAnalytics() {
|
||||
const posthog = usePostHog()
|
||||
|
||||
useEffect(() => {
|
||||
captureEvent(posthog, 'landing_page_viewed', {})
|
||||
}, [posthog])
|
||||
|
||||
return null
|
||||
}
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
Templates,
|
||||
Testimonials,
|
||||
} from '@/app/(home)/components'
|
||||
import { LandingAnalytics } from '@/app/(home)/landing-analytics'
|
||||
|
||||
/**
|
||||
* Landing page root component.
|
||||
@@ -45,6 +46,7 @@ export default async function Landing() {
|
||||
>
|
||||
Skip to main content
|
||||
</a>
|
||||
<LandingAnalytics />
|
||||
<StructuredData />
|
||||
<header>
|
||||
<Navbar blogPosts={blogPosts} />
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
import type { ComponentType, SVGProps } from 'react'
|
||||
import {
|
||||
A2AIcon,
|
||||
AgentMailIcon,
|
||||
AhrefsIcon,
|
||||
AirtableIcon,
|
||||
AirweaveIcon,
|
||||
@@ -189,6 +190,7 @@ type IconComponent = ComponentType<SVGProps<SVGSVGElement>>
|
||||
|
||||
export const blockTypeToIconMap: Record<string, IconComponent> = {
|
||||
a2a: A2AIcon,
|
||||
agentmail: AgentMailIcon,
|
||||
ahrefs: AhrefsIcon,
|
||||
airtable: AirtableIcon,
|
||||
airweave: AirweaveIcon,
|
||||
|
||||
@@ -105,6 +105,109 @@
|
||||
"integrationType": "developer-tools",
|
||||
"tags": ["agentic", "automation"]
|
||||
},
|
||||
{
|
||||
"type": "agentmail",
|
||||
"slug": "agentmail",
|
||||
"name": "AgentMail",
|
||||
"description": "Manage email inboxes, threads, and messages with AgentMail",
|
||||
"longDescription": "Integrate AgentMail into your workflow. Create and manage email inboxes, send and receive messages, reply to threads, manage drafts, and organize threads with labels. Requires API Key.",
|
||||
"bgColor": "#000000",
|
||||
"iconName": "AgentMailIcon",
|
||||
"docsUrl": "https://docs.sim.ai/tools/agentmail",
|
||||
"operations": [
|
||||
{
|
||||
"name": "Send Message",
|
||||
"description": "Send an email message from an AgentMail inbox"
|
||||
},
|
||||
{
|
||||
"name": "Reply to Message",
|
||||
"description": "Reply to an existing email message in AgentMail"
|
||||
},
|
||||
{
|
||||
"name": "Forward Message",
|
||||
"description": "Forward an email message to new recipients in AgentMail"
|
||||
},
|
||||
{
|
||||
"name": "List Threads",
|
||||
"description": "List email threads in AgentMail"
|
||||
},
|
||||
{
|
||||
"name": "Get Thread",
|
||||
"description": "Get details of a specific email thread including messages in AgentMail"
|
||||
},
|
||||
{
|
||||
"name": "Update Thread Labels",
|
||||
"description": "Add or remove labels on an email thread in AgentMail"
|
||||
},
|
||||
{
|
||||
"name": "Delete Thread",
|
||||
"description": "Delete an email thread in AgentMail (moves to trash, or permanently deletes if already in trash)"
|
||||
},
|
||||
{
|
||||
"name": "List Messages",
|
||||
"description": "List messages in an inbox in AgentMail"
|
||||
},
|
||||
{
|
||||
"name": "Get Message",
|
||||
"description": "Get details of a specific email message in AgentMail"
|
||||
},
|
||||
{
|
||||
"name": "Update Message Labels",
|
||||
"description": "Add or remove labels on an email message in AgentMail"
|
||||
},
|
||||
{
|
||||
"name": "Create Draft",
|
||||
"description": "Create a new email draft in AgentMail"
|
||||
},
|
||||
{
|
||||
"name": "List Drafts",
|
||||
"description": "List email drafts in an inbox in AgentMail"
|
||||
},
|
||||
{
|
||||
"name": "Get Draft",
|
||||
"description": "Get details of a specific email draft in AgentMail"
|
||||
},
|
||||
{
|
||||
"name": "Update Draft",
|
||||
"description": "Update an existing email draft in AgentMail"
|
||||
},
|
||||
{
|
||||
"name": "Delete Draft",
|
||||
"description": "Delete an email draft in AgentMail"
|
||||
},
|
||||
{
|
||||
"name": "Send Draft",
|
||||
"description": "Send an existing email draft in AgentMail"
|
||||
},
|
||||
{
|
||||
"name": "Create Inbox",
|
||||
"description": "Create a new email inbox with AgentMail"
|
||||
},
|
||||
{
|
||||
"name": "List Inboxes",
|
||||
"description": "List all email inboxes in AgentMail"
|
||||
},
|
||||
{
|
||||
"name": "Get Inbox",
|
||||
"description": "Get details of a specific email inbox in AgentMail"
|
||||
},
|
||||
{
|
||||
"name": "Update Inbox",
|
||||
"description": "Update the display name of an email inbox in AgentMail"
|
||||
},
|
||||
{
|
||||
"name": "Delete Inbox",
|
||||
"description": "Delete an email inbox in AgentMail"
|
||||
}
|
||||
],
|
||||
"operationCount": 21,
|
||||
"triggers": [],
|
||||
"triggerCount": 0,
|
||||
"authType": "api-key",
|
||||
"category": "tools",
|
||||
"integrationType": "email",
|
||||
"tags": ["messaging"]
|
||||
},
|
||||
{
|
||||
"type": "ahrefs",
|
||||
"slug": "ahrefs",
|
||||
@@ -9717,9 +9820,61 @@
|
||||
{
|
||||
"name": "List Retrospectives",
|
||||
"description": "List incident retrospectives (post-mortems) from Rootly."
|
||||
},
|
||||
{
|
||||
"name": "Delete Incident",
|
||||
"description": "Delete an incident by ID from Rootly."
|
||||
},
|
||||
{
|
||||
"name": "Get Alert",
|
||||
"description": "Retrieve a single alert by ID from Rootly."
|
||||
},
|
||||
{
|
||||
"name": "Update Alert",
|
||||
"description": "Update an existing alert in Rootly."
|
||||
},
|
||||
{
|
||||
"name": "Acknowledge Alert",
|
||||
"description": "Acknowledge an alert in Rootly."
|
||||
},
|
||||
{
|
||||
"name": "Resolve Alert",
|
||||
"description": "Resolve an alert in Rootly."
|
||||
},
|
||||
{
|
||||
"name": "Create Action Item",
|
||||
"description": "Create a new action item for an incident in Rootly."
|
||||
},
|
||||
{
|
||||
"name": "List Action Items",
|
||||
"description": "List action items for an incident in Rootly."
|
||||
},
|
||||
{
|
||||
"name": "List Users",
|
||||
"description": "List users from Rootly with optional search and email filtering."
|
||||
},
|
||||
{
|
||||
"name": "List On-Calls",
|
||||
"description": "List current on-call entries from Rootly with optional filtering."
|
||||
},
|
||||
{
|
||||
"name": "List Schedules",
|
||||
"description": "List on-call schedules from Rootly with optional search filtering."
|
||||
},
|
||||
{
|
||||
"name": "List Escalation Policies",
|
||||
"description": "List escalation policies from Rootly with optional search filtering."
|
||||
},
|
||||
{
|
||||
"name": "List Causes",
|
||||
"description": "List causes from Rootly with optional search filtering."
|
||||
},
|
||||
{
|
||||
"name": "List Playbooks",
|
||||
"description": "List playbooks from Rootly with pagination support."
|
||||
}
|
||||
],
|
||||
"operationCount": 14,
|
||||
"operationCount": 27,
|
||||
"triggers": [],
|
||||
"triggerCount": 0,
|
||||
"authType": "api-key",
|
||||
|
||||
@@ -7,6 +7,7 @@ import { generateAgentCard, generateSkillsFromWorkflow } from '@/lib/a2a/agent-c
|
||||
import type { AgentCapabilities, AgentSkill } from '@/lib/a2a/types'
|
||||
import { checkSessionOrInternalAuth } from '@/lib/auth/hybrid'
|
||||
import { getRedisClient } from '@/lib/core/config/redis'
|
||||
import { captureServerEvent } from '@/lib/posthog/server'
|
||||
import { loadWorkflowFromNormalizedTables } from '@/lib/workflows/persistence/utils'
|
||||
import { checkWorkspaceAccess } from '@/lib/workspaces/permissions/utils'
|
||||
|
||||
@@ -180,6 +181,17 @@ export async function DELETE(request: NextRequest, { params }: { params: Promise
|
||||
|
||||
logger.info(`Deleted A2A agent: ${agentId}`)
|
||||
|
||||
captureServerEvent(
|
||||
auth.userId,
|
||||
'a2a_agent_deleted',
|
||||
{
|
||||
agent_id: agentId,
|
||||
workflow_id: existingAgent.workflowId,
|
||||
workspace_id: existingAgent.workspaceId,
|
||||
},
|
||||
{ groups: { workspace: existingAgent.workspaceId } }
|
||||
)
|
||||
|
||||
return NextResponse.json({ success: true })
|
||||
} catch (error) {
|
||||
logger.error('Error deleting agent:', error)
|
||||
@@ -251,6 +263,16 @@ export async function POST(request: NextRequest, { params }: { params: Promise<R
|
||||
}
|
||||
|
||||
logger.info(`Published A2A agent: ${agentId}`)
|
||||
captureServerEvent(
|
||||
auth.userId,
|
||||
'a2a_agent_published',
|
||||
{
|
||||
agent_id: agentId,
|
||||
workflow_id: existingAgent.workflowId,
|
||||
workspace_id: existingAgent.workspaceId,
|
||||
},
|
||||
{ groups: { workspace: existingAgent.workspaceId } }
|
||||
)
|
||||
return NextResponse.json({ success: true, isPublished: true })
|
||||
}
|
||||
|
||||
@@ -273,6 +295,16 @@ export async function POST(request: NextRequest, { params }: { params: Promise<R
|
||||
}
|
||||
|
||||
logger.info(`Unpublished A2A agent: ${agentId}`)
|
||||
captureServerEvent(
|
||||
auth.userId,
|
||||
'a2a_agent_unpublished',
|
||||
{
|
||||
agent_id: agentId,
|
||||
workflow_id: existingAgent.workflowId,
|
||||
workspace_id: existingAgent.workspaceId,
|
||||
},
|
||||
{ groups: { workspace: existingAgent.workspaceId } }
|
||||
)
|
||||
return NextResponse.json({ success: true, isPublished: false })
|
||||
}
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@ import { generateSkillsFromWorkflow } from '@/lib/a2a/agent-card'
|
||||
import { A2A_DEFAULT_CAPABILITIES } from '@/lib/a2a/constants'
|
||||
import { sanitizeAgentName } from '@/lib/a2a/utils'
|
||||
import { checkSessionOrInternalAuth } from '@/lib/auth/hybrid'
|
||||
import { captureServerEvent } from '@/lib/posthog/server'
|
||||
import { loadWorkflowFromNormalizedTables } from '@/lib/workflows/persistence/utils'
|
||||
import { hasValidStartBlockInState } from '@/lib/workflows/triggers/trigger-utils'
|
||||
import { checkWorkspaceAccess } from '@/lib/workspaces/permissions/utils'
|
||||
@@ -201,6 +202,16 @@ export async function POST(request: NextRequest) {
|
||||
|
||||
logger.info(`Created A2A agent ${agentId} for workflow ${workflowId}`)
|
||||
|
||||
captureServerEvent(
|
||||
auth.userId,
|
||||
'a2a_agent_created',
|
||||
{ agent_id: agentId, workflow_id: workflowId, workspace_id: workspaceId },
|
||||
{
|
||||
groups: { workspace: workspaceId },
|
||||
setOnce: { first_a2a_agent_created_at: new Date().toISOString() },
|
||||
}
|
||||
)
|
||||
|
||||
return NextResponse.json({ success: true, agent }, { status: 201 })
|
||||
} catch (error) {
|
||||
logger.error('Error creating agent:', error)
|
||||
|
||||
@@ -17,6 +17,7 @@ import {
|
||||
hasUsableSubscriptionStatus,
|
||||
} from '@/lib/billing/subscriptions/utils'
|
||||
import { isBillingEnabled } from '@/lib/core/config/feature-flags'
|
||||
import { captureServerEvent } from '@/lib/posthog/server'
|
||||
|
||||
const logger = createLogger('SwitchPlan')
|
||||
|
||||
@@ -173,6 +174,13 @@ export async function POST(request: NextRequest) {
|
||||
interval: targetInterval,
|
||||
})
|
||||
|
||||
captureServerEvent(
|
||||
userId,
|
||||
'subscription_changed',
|
||||
{ from_plan: sub.plan ?? 'unknown', to_plan: targetPlanName, interval: targetInterval },
|
||||
{ set: { plan: targetPlanName } }
|
||||
)
|
||||
|
||||
return NextResponse.json({ success: true, plan: targetPlanName, interval: targetInterval })
|
||||
} catch (error) {
|
||||
logger.error('Failed to switch subscription', {
|
||||
|
||||
@@ -27,6 +27,7 @@ import {
|
||||
createRequestTracker,
|
||||
createUnauthorizedResponse,
|
||||
} from '@/lib/copilot/request-helpers'
|
||||
import { captureServerEvent } from '@/lib/posthog/server'
|
||||
import {
|
||||
authorizeWorkflowByWorkspacePermission,
|
||||
resolveWorkflowIdForUser,
|
||||
@@ -188,6 +189,22 @@ export async function POST(req: NextRequest) {
|
||||
.warn('Failed to resolve workspaceId from workflow')
|
||||
}
|
||||
|
||||
captureServerEvent(
|
||||
authenticatedUserId,
|
||||
'copilot_chat_sent',
|
||||
{
|
||||
workflow_id: workflowId,
|
||||
workspace_id: resolvedWorkspaceId ?? '',
|
||||
has_file_attachments: Array.isArray(fileAttachments) && fileAttachments.length > 0,
|
||||
has_contexts: Array.isArray(contexts) && contexts.length > 0,
|
||||
mode,
|
||||
},
|
||||
{
|
||||
groups: resolvedWorkspaceId ? { workspace: resolvedWorkspaceId } : undefined,
|
||||
setOnce: { first_copilot_use_at: new Date().toISOString() },
|
||||
}
|
||||
)
|
||||
|
||||
const userMessageIdToUse = userMessageId || crypto.randomUUID()
|
||||
const reqLogger = logger.withMetadata({
|
||||
requestId: tracker.requestId,
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
createRequestTracker,
|
||||
createUnauthorizedResponse,
|
||||
} from '@/lib/copilot/request-helpers'
|
||||
import { captureServerEvent } from '@/lib/posthog/server'
|
||||
|
||||
const logger = createLogger('CopilotFeedbackAPI')
|
||||
|
||||
@@ -76,6 +77,12 @@ export async function POST(req: NextRequest) {
|
||||
duration: tracker.getDuration(),
|
||||
})
|
||||
|
||||
captureServerEvent(authenticatedUserId, 'copilot_feedback_submitted', {
|
||||
is_positive: isPositiveFeedback,
|
||||
has_text_feedback: !!feedback,
|
||||
has_workflow_yaml: !!workflowYaml,
|
||||
})
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
feedbackId: feedbackRecord.feedbackId,
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
syncPersonalEnvCredentialsForUser,
|
||||
syncWorkspaceEnvCredentials,
|
||||
} from '@/lib/credentials/environment'
|
||||
import { captureServerEvent } from '@/lib/posthog/server'
|
||||
|
||||
const logger = createLogger('CredentialByIdAPI')
|
||||
|
||||
@@ -236,6 +237,17 @@ export async function DELETE(
|
||||
envKeys: Object.keys(current),
|
||||
})
|
||||
|
||||
captureServerEvent(
|
||||
session.user.id,
|
||||
'credential_deleted',
|
||||
{
|
||||
credential_type: 'env_personal',
|
||||
provider_id: access.credential.envKey,
|
||||
workspace_id: access.credential.workspaceId,
|
||||
},
|
||||
{ groups: { workspace: access.credential.workspaceId } }
|
||||
)
|
||||
|
||||
return NextResponse.json({ success: true }, { status: 200 })
|
||||
}
|
||||
|
||||
@@ -278,10 +290,33 @@ export async function DELETE(
|
||||
actingUserId: session.user.id,
|
||||
})
|
||||
|
||||
captureServerEvent(
|
||||
session.user.id,
|
||||
'credential_deleted',
|
||||
{
|
||||
credential_type: 'env_workspace',
|
||||
provider_id: access.credential.envKey,
|
||||
workspace_id: access.credential.workspaceId,
|
||||
},
|
||||
{ groups: { workspace: access.credential.workspaceId } }
|
||||
)
|
||||
|
||||
return NextResponse.json({ success: true }, { status: 200 })
|
||||
}
|
||||
|
||||
await db.delete(credential).where(eq(credential.id, id))
|
||||
|
||||
captureServerEvent(
|
||||
session.user.id,
|
||||
'credential_deleted',
|
||||
{
|
||||
credential_type: access.credential.type as 'oauth' | 'service_account',
|
||||
provider_id: access.credential.providerId ?? id,
|
||||
workspace_id: access.credential.workspaceId,
|
||||
},
|
||||
{ groups: { workspace: access.credential.workspaceId } }
|
||||
)
|
||||
|
||||
return NextResponse.json({ success: true }, { status: 200 })
|
||||
} catch (error) {
|
||||
logger.error('Failed to delete credential', error)
|
||||
|
||||
@@ -10,6 +10,7 @@ import { generateRequestId } from '@/lib/core/utils/request'
|
||||
import { getWorkspaceMemberUserIds } from '@/lib/credentials/environment'
|
||||
import { syncWorkspaceOAuthCredentialsForUser } from '@/lib/credentials/oauth'
|
||||
import { getServiceConfigByProviderId } from '@/lib/oauth'
|
||||
import { captureServerEvent } from '@/lib/posthog/server'
|
||||
import { checkWorkspaceAccess } from '@/lib/workspaces/permissions/utils'
|
||||
import { isValidEnvVarName } from '@/executor/constants'
|
||||
|
||||
@@ -600,6 +601,16 @@ export async function POST(request: NextRequest) {
|
||||
.where(eq(credential.id, credentialId))
|
||||
.limit(1)
|
||||
|
||||
captureServerEvent(
|
||||
session.user.id,
|
||||
'credential_connected',
|
||||
{ credential_type: type, provider_id: resolvedProviderId ?? type, workspace_id: workspaceId },
|
||||
{
|
||||
groups: { workspace: workspaceId },
|
||||
setOnce: { first_credential_connected_at: new Date().toISOString() },
|
||||
}
|
||||
)
|
||||
|
||||
return NextResponse.json({ credential: created }, { status: 201 })
|
||||
} catch (error: any) {
|
||||
if (error?.code === '23505') {
|
||||
|
||||
@@ -16,6 +16,7 @@ import { checkSessionOrInternalAuth } from '@/lib/auth/hybrid'
|
||||
import { generateRequestId } from '@/lib/core/utils/request'
|
||||
import { deleteDocumentStorageFiles } from '@/lib/knowledge/documents/service'
|
||||
import { cleanupUnusedTagDefinitions } from '@/lib/knowledge/tags/service'
|
||||
import { captureServerEvent } from '@/lib/posthog/server'
|
||||
import { refreshAccessTokenIfNeeded } from '@/app/api/auth/oauth/utils'
|
||||
import { checkKnowledgeBaseAccess, checkKnowledgeBaseWriteAccess } from '@/app/api/knowledge/utils'
|
||||
import { CONNECTOR_REGISTRY } from '@/connectors/registry'
|
||||
@@ -351,6 +352,19 @@ export async function DELETE(request: NextRequest, { params }: RouteParams) {
|
||||
`[${requestId}] Deleted connector ${connectorId}${deleteDocuments ? ` and ${docCount} documents` : `, kept ${docCount} documents`}`
|
||||
)
|
||||
|
||||
const kbWorkspaceId = writeCheck.knowledgeBase.workspaceId ?? ''
|
||||
captureServerEvent(
|
||||
auth.userId,
|
||||
'knowledge_base_connector_removed',
|
||||
{
|
||||
knowledge_base_id: knowledgeBaseId,
|
||||
workspace_id: kbWorkspaceId,
|
||||
connector_type: existingConnector[0].connectorType,
|
||||
documents_deleted: deleteDocuments ? docCount : 0,
|
||||
},
|
||||
kbWorkspaceId ? { groups: { workspace: kbWorkspaceId } } : undefined
|
||||
)
|
||||
|
||||
recordAudit({
|
||||
workspaceId: writeCheck.knowledgeBase.workspaceId,
|
||||
actorId: auth.userId,
|
||||
|
||||
@@ -7,6 +7,7 @@ import { AuditAction, AuditResourceType, recordAudit } from '@/lib/audit/log'
|
||||
import { checkSessionOrInternalAuth } from '@/lib/auth/hybrid'
|
||||
import { generateRequestId } from '@/lib/core/utils/request'
|
||||
import { dispatchSync } from '@/lib/knowledge/connectors/sync-engine'
|
||||
import { captureServerEvent } from '@/lib/posthog/server'
|
||||
import { checkKnowledgeBaseWriteAccess } from '@/app/api/knowledge/utils'
|
||||
|
||||
const logger = createLogger('ConnectorManualSyncAPI')
|
||||
@@ -55,6 +56,18 @@ export async function POST(request: NextRequest, { params }: RouteParams) {
|
||||
|
||||
logger.info(`[${requestId}] Manual sync triggered for connector ${connectorId}`)
|
||||
|
||||
const kbWorkspaceId = writeCheck.knowledgeBase.workspaceId ?? ''
|
||||
captureServerEvent(
|
||||
auth.userId,
|
||||
'knowledge_base_connector_synced',
|
||||
{
|
||||
knowledge_base_id: knowledgeBaseId,
|
||||
workspace_id: kbWorkspaceId,
|
||||
connector_type: connectorRows[0].connectorType,
|
||||
},
|
||||
kbWorkspaceId ? { groups: { workspace: kbWorkspaceId } } : undefined
|
||||
)
|
||||
|
||||
recordAudit({
|
||||
workspaceId: writeCheck.knowledgeBase.workspaceId,
|
||||
actorId: auth.userId,
|
||||
|
||||
@@ -11,6 +11,7 @@ import { generateRequestId } from '@/lib/core/utils/request'
|
||||
import { dispatchSync } from '@/lib/knowledge/connectors/sync-engine'
|
||||
import { allocateTagSlots } from '@/lib/knowledge/constants'
|
||||
import { createTagDefinition } from '@/lib/knowledge/tags/service'
|
||||
import { captureServerEvent } from '@/lib/posthog/server'
|
||||
import { getCredential } from '@/app/api/auth/oauth/utils'
|
||||
import { checkKnowledgeBaseAccess, checkKnowledgeBaseWriteAccess } from '@/app/api/knowledge/utils'
|
||||
import { CONNECTOR_REGISTRY } from '@/connectors/registry'
|
||||
@@ -227,6 +228,22 @@ export async function POST(request: NextRequest, { params }: { params: Promise<{
|
||||
|
||||
logger.info(`[${requestId}] Created connector ${connectorId} for KB ${knowledgeBaseId}`)
|
||||
|
||||
const kbWorkspaceId = writeCheck.knowledgeBase.workspaceId ?? ''
|
||||
captureServerEvent(
|
||||
auth.userId,
|
||||
'knowledge_base_connector_added',
|
||||
{
|
||||
knowledge_base_id: knowledgeBaseId,
|
||||
workspace_id: kbWorkspaceId,
|
||||
connector_type: connectorType,
|
||||
sync_interval_minutes: syncIntervalMinutes,
|
||||
},
|
||||
{
|
||||
groups: kbWorkspaceId ? { workspace: kbWorkspaceId } : undefined,
|
||||
setOnce: { first_connector_added_at: new Date().toISOString() },
|
||||
}
|
||||
)
|
||||
|
||||
recordAudit({
|
||||
workspaceId: writeCheck.knowledgeBase.workspaceId,
|
||||
actorId: auth.userId,
|
||||
|
||||
@@ -16,6 +16,7 @@ import {
|
||||
type TagFilterCondition,
|
||||
} from '@/lib/knowledge/documents/service'
|
||||
import type { DocumentSortField, SortOrder } from '@/lib/knowledge/documents/types'
|
||||
import { captureServerEvent } from '@/lib/posthog/server'
|
||||
import { authorizeWorkflowByWorkspacePermission } from '@/lib/workflows/utils'
|
||||
import { checkKnowledgeBaseAccess, checkKnowledgeBaseWriteAccess } from '@/app/api/knowledge/utils'
|
||||
|
||||
@@ -214,6 +215,8 @@ export async function POST(req: NextRequest, { params }: { params: Promise<{ id:
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
const kbWorkspaceId = accessCheck.knowledgeBase?.workspaceId
|
||||
|
||||
if (body.bulk === true) {
|
||||
try {
|
||||
const validatedData = BulkCreateDocumentsSchema.parse(body)
|
||||
@@ -240,6 +243,21 @@ export async function POST(req: NextRequest, { params }: { params: Promise<{ id:
|
||||
// Silently fail
|
||||
}
|
||||
|
||||
captureServerEvent(
|
||||
userId,
|
||||
'knowledge_base_document_uploaded',
|
||||
{
|
||||
knowledge_base_id: knowledgeBaseId,
|
||||
workspace_id: kbWorkspaceId ?? '',
|
||||
document_count: createdDocuments.length,
|
||||
upload_type: 'bulk',
|
||||
},
|
||||
{
|
||||
...(kbWorkspaceId ? { groups: { workspace: kbWorkspaceId } } : {}),
|
||||
setOnce: { first_document_uploaded_at: new Date().toISOString() },
|
||||
}
|
||||
)
|
||||
|
||||
processDocumentsWithQueue(
|
||||
createdDocuments,
|
||||
knowledgeBaseId,
|
||||
@@ -314,6 +332,21 @@ export async function POST(req: NextRequest, { params }: { params: Promise<{ id:
|
||||
// Silently fail
|
||||
}
|
||||
|
||||
captureServerEvent(
|
||||
userId,
|
||||
'knowledge_base_document_uploaded',
|
||||
{
|
||||
knowledge_base_id: knowledgeBaseId,
|
||||
workspace_id: kbWorkspaceId ?? '',
|
||||
document_count: 1,
|
||||
upload_type: 'single',
|
||||
},
|
||||
{
|
||||
...(kbWorkspaceId ? { groups: { workspace: kbWorkspaceId } } : {}),
|
||||
setOnce: { first_document_uploaded_at: new Date().toISOString() },
|
||||
}
|
||||
)
|
||||
|
||||
recordAudit({
|
||||
workspaceId: accessCheck.knowledgeBase?.workspaceId ?? null,
|
||||
actorId: userId,
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
KnowledgeBaseConflictError,
|
||||
type KnowledgeBaseScope,
|
||||
} from '@/lib/knowledge/service'
|
||||
import { captureServerEvent } from '@/lib/posthog/server'
|
||||
|
||||
const logger = createLogger('KnowledgeBaseAPI')
|
||||
|
||||
@@ -115,6 +116,20 @@ export async function POST(req: NextRequest) {
|
||||
// Telemetry should not fail the operation
|
||||
}
|
||||
|
||||
captureServerEvent(
|
||||
session.user.id,
|
||||
'knowledge_base_created',
|
||||
{
|
||||
knowledge_base_id: newKnowledgeBase.id,
|
||||
workspace_id: validatedData.workspaceId,
|
||||
name: validatedData.name,
|
||||
},
|
||||
{
|
||||
groups: { workspace: validatedData.workspaceId },
|
||||
setOnce: { first_kb_created_at: new Date().toISOString() },
|
||||
}
|
||||
)
|
||||
|
||||
logger.info(
|
||||
`[${requestId}] Knowledge base created: ${newKnowledgeBase.id} for user ${session.user.id}`
|
||||
)
|
||||
|
||||
@@ -18,6 +18,7 @@ import {
|
||||
createMcpSuccessResponse,
|
||||
generateMcpServerId,
|
||||
} from '@/lib/mcp/utils'
|
||||
import { captureServerEvent } from '@/lib/posthog/server'
|
||||
|
||||
const logger = createLogger('McpServersAPI')
|
||||
|
||||
@@ -180,6 +181,20 @@ export const POST = withMcpAuth('write')(
|
||||
// Silently fail
|
||||
}
|
||||
|
||||
const sourceParam = body.source as string | undefined
|
||||
const source =
|
||||
sourceParam === 'settings' || sourceParam === 'tool_input' ? sourceParam : undefined
|
||||
|
||||
captureServerEvent(
|
||||
userId,
|
||||
'mcp_server_connected',
|
||||
{ workspace_id: workspaceId, server_name: body.name, transport: body.transport, source },
|
||||
{
|
||||
groups: { workspace: workspaceId },
|
||||
setOnce: { first_mcp_connected_at: new Date().toISOString() },
|
||||
}
|
||||
)
|
||||
|
||||
recordAudit({
|
||||
workspaceId,
|
||||
actorId: userId,
|
||||
@@ -214,6 +229,9 @@ export const DELETE = withMcpAuth('admin')(
|
||||
try {
|
||||
const { searchParams } = new URL(request.url)
|
||||
const serverId = searchParams.get('serverId')
|
||||
const sourceParam = searchParams.get('source')
|
||||
const source =
|
||||
sourceParam === 'settings' || sourceParam === 'tool_input' ? sourceParam : undefined
|
||||
|
||||
if (!serverId) {
|
||||
return createMcpErrorResponse(
|
||||
@@ -242,6 +260,13 @@ export const DELETE = withMcpAuth('admin')(
|
||||
|
||||
logger.info(`[${requestId}] Successfully deleted MCP server: ${serverId}`)
|
||||
|
||||
captureServerEvent(
|
||||
userId,
|
||||
'mcp_server_disconnected',
|
||||
{ workspace_id: workspaceId, server_name: deletedServer.name, source },
|
||||
{ groups: { workspace: workspaceId } }
|
||||
)
|
||||
|
||||
recordAudit({
|
||||
workspaceId,
|
||||
actorId: userId,
|
||||
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
createUnauthorizedResponse,
|
||||
} from '@/lib/copilot/request-helpers'
|
||||
import { taskPubSub } from '@/lib/copilot/task-events'
|
||||
import { captureServerEvent } from '@/lib/posthog/server'
|
||||
|
||||
const logger = createLogger('MothershipChatAPI')
|
||||
|
||||
@@ -142,12 +143,41 @@ export async function PATCH(
|
||||
return NextResponse.json({ success: false, error: 'Chat not found' }, { status: 404 })
|
||||
}
|
||||
|
||||
if (title !== undefined && updatedChat.workspaceId) {
|
||||
taskPubSub?.publishStatusChanged({
|
||||
workspaceId: updatedChat.workspaceId,
|
||||
chatId,
|
||||
type: 'renamed',
|
||||
})
|
||||
if (updatedChat.workspaceId) {
|
||||
if (title !== undefined) {
|
||||
taskPubSub?.publishStatusChanged({
|
||||
workspaceId: updatedChat.workspaceId,
|
||||
chatId,
|
||||
type: 'renamed',
|
||||
})
|
||||
captureServerEvent(
|
||||
userId,
|
||||
'task_renamed',
|
||||
{ workspace_id: updatedChat.workspaceId },
|
||||
{
|
||||
groups: { workspace: updatedChat.workspaceId },
|
||||
}
|
||||
)
|
||||
}
|
||||
if (isUnread === false) {
|
||||
captureServerEvent(
|
||||
userId,
|
||||
'task_marked_read',
|
||||
{ workspace_id: updatedChat.workspaceId },
|
||||
{
|
||||
groups: { workspace: updatedChat.workspaceId },
|
||||
}
|
||||
)
|
||||
} else if (isUnread === true) {
|
||||
captureServerEvent(
|
||||
userId,
|
||||
'task_marked_unread',
|
||||
{ workspace_id: updatedChat.workspaceId },
|
||||
{
|
||||
groups: { workspace: updatedChat.workspaceId },
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return NextResponse.json({ success: true })
|
||||
@@ -203,6 +233,14 @@ export async function DELETE(
|
||||
chatId,
|
||||
type: 'deleted',
|
||||
})
|
||||
captureServerEvent(
|
||||
userId,
|
||||
'task_deleted',
|
||||
{ workspace_id: deletedChat.workspaceId },
|
||||
{
|
||||
groups: { workspace: deletedChat.workspaceId },
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
return NextResponse.json({ success: true })
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
createUnauthorizedResponse,
|
||||
} from '@/lib/copilot/request-helpers'
|
||||
import { taskPubSub } from '@/lib/copilot/task-events'
|
||||
import { captureServerEvent } from '@/lib/posthog/server'
|
||||
import { assertActiveWorkspaceAccess } from '@/lib/workspaces/permissions/utils'
|
||||
|
||||
const logger = createLogger('MothershipChatsAPI')
|
||||
@@ -95,6 +96,15 @@ export async function POST(request: NextRequest) {
|
||||
|
||||
taskPubSub?.publishStatusChanged({ workspaceId, chatId: chat.id, type: 'created' })
|
||||
|
||||
captureServerEvent(
|
||||
userId,
|
||||
'task_created',
|
||||
{ workspace_id: workspaceId },
|
||||
{
|
||||
groups: { workspace: workspaceId },
|
||||
}
|
||||
)
|
||||
|
||||
return NextResponse.json({ success: true, id: chat.id })
|
||||
} catch (error) {
|
||||
if (error instanceof z.ZodError) {
|
||||
|
||||
@@ -4,6 +4,7 @@ import { z } from 'zod'
|
||||
import { AuditAction, AuditResourceType, recordAudit } from '@/lib/audit/log'
|
||||
import { checkSessionOrInternalAuth } from '@/lib/auth/hybrid'
|
||||
import { generateRequestId } from '@/lib/core/utils/request'
|
||||
import { captureServerEvent } from '@/lib/posthog/server'
|
||||
import { deleteSkill, listSkills, upsertSkills } from '@/lib/workflows/skills/operations'
|
||||
import { getUserEntityPermissions } from '@/lib/workspaces/permissions/utils'
|
||||
|
||||
@@ -23,6 +24,7 @@ const SkillSchema = z.object({
|
||||
})
|
||||
),
|
||||
workspaceId: z.string().optional(),
|
||||
source: z.enum(['settings', 'tool_input']).optional(),
|
||||
})
|
||||
|
||||
/** GET - Fetch all skills for a workspace */
|
||||
@@ -75,7 +77,7 @@ export async function POST(req: NextRequest) {
|
||||
const body = await req.json()
|
||||
|
||||
try {
|
||||
const { skills, workspaceId } = SkillSchema.parse(body)
|
||||
const { skills, workspaceId, source } = SkillSchema.parse(body)
|
||||
|
||||
if (!workspaceId) {
|
||||
logger.warn(`[${requestId}] Missing workspaceId in request body`)
|
||||
@@ -107,6 +109,12 @@ export async function POST(req: NextRequest) {
|
||||
resourceName: skill.name,
|
||||
description: `Created/updated skill "${skill.name}"`,
|
||||
})
|
||||
captureServerEvent(
|
||||
userId,
|
||||
'skill_created',
|
||||
{ skill_id: skill.id, skill_name: skill.name, workspace_id: workspaceId, source },
|
||||
{ groups: { workspace: workspaceId } }
|
||||
)
|
||||
}
|
||||
|
||||
return NextResponse.json({ success: true, data: resultSkills })
|
||||
@@ -137,6 +145,9 @@ export async function DELETE(request: NextRequest) {
|
||||
const searchParams = request.nextUrl.searchParams
|
||||
const skillId = searchParams.get('id')
|
||||
const workspaceId = searchParams.get('workspaceId')
|
||||
const sourceParam = searchParams.get('source')
|
||||
const source =
|
||||
sourceParam === 'settings' || sourceParam === 'tool_input' ? sourceParam : undefined
|
||||
|
||||
try {
|
||||
const authResult = await checkSessionOrInternalAuth(request, { requireWorkflowId: false })
|
||||
@@ -180,6 +191,13 @@ export async function DELETE(request: NextRequest) {
|
||||
description: `Deleted skill`,
|
||||
})
|
||||
|
||||
captureServerEvent(
|
||||
userId,
|
||||
'skill_deleted',
|
||||
{ skill_id: skillId, workspace_id: workspaceId, source },
|
||||
{ groups: { workspace: workspaceId } }
|
||||
)
|
||||
|
||||
logger.info(`[${requestId}] Deleted skill: ${skillId}`)
|
||||
return NextResponse.json({ success: true })
|
||||
} catch (error) {
|
||||
|
||||
@@ -3,6 +3,7 @@ import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { z } from 'zod'
|
||||
import { checkSessionOrInternalAuth } from '@/lib/auth/hybrid'
|
||||
import { generateRequestId } from '@/lib/core/utils/request'
|
||||
import { captureServerEvent } from '@/lib/posthog/server'
|
||||
import {
|
||||
deleteTable,
|
||||
NAME_PATTERN,
|
||||
@@ -183,6 +184,13 @@ export async function DELETE(request: NextRequest, { params }: TableRouteParams)
|
||||
|
||||
await deleteTable(tableId, requestId)
|
||||
|
||||
captureServerEvent(
|
||||
authResult.userId,
|
||||
'table_deleted',
|
||||
{ table_id: tableId, workspace_id: table.workspaceId },
|
||||
{ groups: { workspace: table.workspaceId } }
|
||||
)
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
data: {
|
||||
|
||||
@@ -3,6 +3,7 @@ import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { z } from 'zod'
|
||||
import { checkSessionOrInternalAuth } from '@/lib/auth/hybrid'
|
||||
import { generateRequestId } from '@/lib/core/utils/request'
|
||||
import { captureServerEvent } from '@/lib/posthog/server'
|
||||
import {
|
||||
createTable,
|
||||
getWorkspaceTableLimits,
|
||||
@@ -141,6 +142,20 @@ export async function POST(request: NextRequest) {
|
||||
requestId
|
||||
)
|
||||
|
||||
captureServerEvent(
|
||||
authResult.userId,
|
||||
'table_created',
|
||||
{
|
||||
table_id: table.id,
|
||||
workspace_id: params.workspaceId,
|
||||
column_count: params.schema.columns.length,
|
||||
},
|
||||
{
|
||||
groups: { workspace: params.workspaceId },
|
||||
setOnce: { first_table_created_at: new Date().toISOString() },
|
||||
}
|
||||
)
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
data: {
|
||||
|
||||
@@ -7,6 +7,7 @@ import { z } from 'zod'
|
||||
import { AuditAction, AuditResourceType, recordAudit } from '@/lib/audit/log'
|
||||
import { checkSessionOrInternalAuth } from '@/lib/auth/hybrid'
|
||||
import { generateRequestId } from '@/lib/core/utils/request'
|
||||
import { captureServerEvent } from '@/lib/posthog/server'
|
||||
import { upsertCustomTools } from '@/lib/workflows/custom-tools/operations'
|
||||
import { authorizeWorkflowByWorkspacePermission } from '@/lib/workflows/utils'
|
||||
import { getUserEntityPermissions } from '@/lib/workspaces/permissions/utils'
|
||||
@@ -34,6 +35,7 @@ const CustomToolSchema = z.object({
|
||||
})
|
||||
),
|
||||
workspaceId: z.string().optional(),
|
||||
source: z.enum(['settings', 'tool_input']).optional(),
|
||||
})
|
||||
|
||||
// GET - Fetch all custom tools for the workspace
|
||||
@@ -135,7 +137,7 @@ export async function POST(req: NextRequest) {
|
||||
|
||||
try {
|
||||
// Validate the request body
|
||||
const { tools, workspaceId } = CustomToolSchema.parse(body)
|
||||
const { tools, workspaceId, source } = CustomToolSchema.parse(body)
|
||||
|
||||
if (!workspaceId) {
|
||||
logger.warn(`[${requestId}] Missing workspaceId in request body`)
|
||||
@@ -168,6 +170,16 @@ export async function POST(req: NextRequest) {
|
||||
})
|
||||
|
||||
for (const tool of resultTools) {
|
||||
captureServerEvent(
|
||||
userId,
|
||||
'custom_tool_saved',
|
||||
{ tool_id: tool.id, workspace_id: workspaceId, tool_name: tool.title, source },
|
||||
{
|
||||
groups: { workspace: workspaceId },
|
||||
setOnce: { first_custom_tool_saved_at: new Date().toISOString() },
|
||||
}
|
||||
)
|
||||
|
||||
recordAudit({
|
||||
workspaceId,
|
||||
actorId: userId,
|
||||
@@ -205,6 +217,9 @@ export async function DELETE(request: NextRequest) {
|
||||
const searchParams = request.nextUrl.searchParams
|
||||
const toolId = searchParams.get('id')
|
||||
const workspaceId = searchParams.get('workspaceId')
|
||||
const sourceParam = searchParams.get('source')
|
||||
const source =
|
||||
sourceParam === 'settings' || sourceParam === 'tool_input' ? sourceParam : undefined
|
||||
|
||||
if (!toolId) {
|
||||
logger.warn(`[${requestId}] Missing tool ID for deletion`)
|
||||
@@ -278,6 +293,14 @@ export async function DELETE(request: NextRequest) {
|
||||
// Delete the tool
|
||||
await db.delete(customTools).where(eq(customTools.id, toolId))
|
||||
|
||||
const toolWorkspaceId = tool.workspaceId ?? workspaceId ?? ''
|
||||
captureServerEvent(
|
||||
userId,
|
||||
'custom_tool_deleted',
|
||||
{ tool_id: toolId, workspace_id: toolWorkspaceId, source },
|
||||
toolWorkspaceId ? { groups: { workspace: toolWorkspaceId } } : undefined
|
||||
)
|
||||
|
||||
recordAudit({
|
||||
workspaceId: tool.workspaceId || undefined,
|
||||
actorId: userId,
|
||||
|
||||
@@ -8,6 +8,7 @@ import { checkSessionOrInternalAuth } from '@/lib/auth/hybrid'
|
||||
import { validateInteger } from '@/lib/core/security/input-validation'
|
||||
import { PlatformEvents } from '@/lib/core/telemetry'
|
||||
import { generateRequestId } from '@/lib/core/utils/request'
|
||||
import { captureServerEvent } from '@/lib/posthog/server'
|
||||
import { cleanupExternalWebhook } from '@/lib/webhooks/provider-subscriptions'
|
||||
import { authorizeWorkflowByWorkspacePermission } from '@/lib/workflows/utils'
|
||||
|
||||
@@ -274,6 +275,19 @@ export async function DELETE(
|
||||
request,
|
||||
})
|
||||
|
||||
const wsId = webhookData.workflow.workspaceId || undefined
|
||||
captureServerEvent(
|
||||
userId,
|
||||
'webhook_trigger_deleted',
|
||||
{
|
||||
webhook_id: id,
|
||||
workflow_id: webhookData.workflow.id,
|
||||
provider: foundWebhook.provider || 'generic',
|
||||
workspace_id: wsId ?? '',
|
||||
},
|
||||
wsId ? { groups: { workspace: wsId } } : undefined
|
||||
)
|
||||
|
||||
return NextResponse.json({ success: true }, { status: 200 })
|
||||
} catch (error: any) {
|
||||
logger.error(`[${requestId}] Error deleting webhook`, {
|
||||
|
||||
@@ -9,6 +9,7 @@ import { getSession } from '@/lib/auth'
|
||||
import { PlatformEvents } from '@/lib/core/telemetry'
|
||||
import { generateRequestId } from '@/lib/core/utils/request'
|
||||
import { getProviderIdFromServiceId } from '@/lib/oauth'
|
||||
import { captureServerEvent } from '@/lib/posthog/server'
|
||||
import { resolveEnvVarsInObject } from '@/lib/webhooks/env-resolver'
|
||||
import {
|
||||
cleanupExternalWebhook,
|
||||
@@ -763,6 +764,19 @@ export async function POST(request: NextRequest) {
|
||||
metadata: { provider, workflowId },
|
||||
request,
|
||||
})
|
||||
|
||||
const wsId = workflowRecord.workspaceId || undefined
|
||||
captureServerEvent(
|
||||
userId,
|
||||
'webhook_trigger_created',
|
||||
{
|
||||
webhook_id: savedWebhook.id,
|
||||
workflow_id: workflowId,
|
||||
provider: provider || 'generic',
|
||||
workspace_id: wsId ?? '',
|
||||
},
|
||||
wsId ? { groups: { workspace: wsId } } : undefined
|
||||
)
|
||||
}
|
||||
|
||||
const status = targetWebhookId ? 200 : 201
|
||||
|
||||
@@ -3,6 +3,7 @@ import { createLogger } from '@sim/logger'
|
||||
import { eq } from 'drizzle-orm'
|
||||
import type { NextRequest } from 'next/server'
|
||||
import { generateRequestId } from '@/lib/core/utils/request'
|
||||
import { captureServerEvent } from '@/lib/posthog/server'
|
||||
import { performFullDeploy, performFullUndeploy } from '@/lib/workflows/orchestration'
|
||||
import { validateWorkflowPermissions } from '@/lib/workflows/utils'
|
||||
import {
|
||||
@@ -96,6 +97,16 @@ export async function POST(request: NextRequest, { params }: { params: Promise<{
|
||||
|
||||
logger.info(`[${requestId}] Workflow deployed successfully: ${id}`)
|
||||
|
||||
captureServerEvent(
|
||||
actorUserId,
|
||||
'workflow_deployed',
|
||||
{ workflow_id: id, workspace_id: workflowData!.workspaceId ?? '' },
|
||||
{
|
||||
groups: workflowData!.workspaceId ? { workspace: workflowData!.workspaceId } : undefined,
|
||||
setOnce: { first_workflow_deployed_at: new Date().toISOString() },
|
||||
}
|
||||
)
|
||||
|
||||
const responseApiKeyInfo = workflowData!.workspaceId
|
||||
? 'Workspace API keys'
|
||||
: 'Personal API keys'
|
||||
@@ -118,7 +129,11 @@ export async function PATCH(request: NextRequest, { params }: { params: Promise<
|
||||
const { id } = await params
|
||||
|
||||
try {
|
||||
const { error, session } = await validateWorkflowPermissions(id, requestId, 'admin')
|
||||
const {
|
||||
error,
|
||||
session,
|
||||
workflow: workflowData,
|
||||
} = await validateWorkflowPermissions(id, requestId, 'admin')
|
||||
if (error) {
|
||||
return createErrorResponse(error.message, error.status)
|
||||
}
|
||||
@@ -148,6 +163,14 @@ export async function PATCH(request: NextRequest, { params }: { params: Promise<
|
||||
|
||||
logger.info(`[${requestId}] Updated isPublicApi for workflow ${id} to ${isPublicApi}`)
|
||||
|
||||
const wsId = workflowData?.workspaceId
|
||||
captureServerEvent(
|
||||
session!.user.id,
|
||||
'workflow_public_api_toggled',
|
||||
{ workflow_id: id, workspace_id: wsId ?? '', is_public: isPublicApi },
|
||||
wsId ? { groups: { workspace: wsId } } : undefined
|
||||
)
|
||||
|
||||
return createSuccessResponse({ isPublicApi })
|
||||
} catch (error: unknown) {
|
||||
const message = error instanceof Error ? error.message : 'Failed to update deployment settings'
|
||||
@@ -164,7 +187,11 @@ export async function DELETE(
|
||||
const { id } = await params
|
||||
|
||||
try {
|
||||
const { error, session } = await validateWorkflowPermissions(id, requestId, 'admin')
|
||||
const {
|
||||
error,
|
||||
session,
|
||||
workflow: workflowData,
|
||||
} = await validateWorkflowPermissions(id, requestId, 'admin')
|
||||
if (error) {
|
||||
return createErrorResponse(error.message, error.status)
|
||||
}
|
||||
@@ -179,6 +206,14 @@ export async function DELETE(
|
||||
return createErrorResponse(result.error || 'Failed to undeploy workflow', 500)
|
||||
}
|
||||
|
||||
const wsId = workflowData?.workspaceId
|
||||
captureServerEvent(
|
||||
session!.user.id,
|
||||
'workflow_undeployed',
|
||||
{ workflow_id: id, workspace_id: wsId ?? '' },
|
||||
wsId ? { groups: { workspace: wsId } } : undefined
|
||||
)
|
||||
|
||||
return createSuccessResponse({
|
||||
isDeployed: false,
|
||||
deployedAt: null,
|
||||
|
||||
@@ -5,6 +5,7 @@ import type { NextRequest } from 'next/server'
|
||||
import { AuditAction, AuditResourceType, recordAudit } from '@/lib/audit/log'
|
||||
import { env } from '@/lib/core/config/env'
|
||||
import { generateRequestId } from '@/lib/core/utils/request'
|
||||
import { captureServerEvent } from '@/lib/posthog/server'
|
||||
import { saveWorkflowToNormalizedTables } from '@/lib/workflows/persistence/utils'
|
||||
import { validateWorkflowPermissions } from '@/lib/workflows/utils'
|
||||
import { createErrorResponse, createSuccessResponse } from '@/app/api/workflows/utils'
|
||||
@@ -104,6 +105,19 @@ export async function POST(
|
||||
logger.error('Error sending workflow reverted event to socket server', e)
|
||||
}
|
||||
|
||||
captureServerEvent(
|
||||
session!.user.id,
|
||||
'workflow_deployment_reverted',
|
||||
{
|
||||
workflow_id: id,
|
||||
workspace_id: workflowRecord?.workspaceId ?? '',
|
||||
version,
|
||||
},
|
||||
workflowRecord?.workspaceId
|
||||
? { groups: { workspace: workflowRecord.workspaceId } }
|
||||
: undefined
|
||||
)
|
||||
|
||||
recordAudit({
|
||||
workspaceId: workflowRecord?.workspaceId ?? null,
|
||||
actorId: session!.user.id,
|
||||
|
||||
@@ -4,6 +4,7 @@ import { and, eq } from 'drizzle-orm'
|
||||
import type { NextRequest } from 'next/server'
|
||||
import { z } from 'zod'
|
||||
import { generateRequestId } from '@/lib/core/utils/request'
|
||||
import { captureServerEvent } from '@/lib/posthog/server'
|
||||
import { performActivateVersion } from '@/lib/workflows/orchestration'
|
||||
import { validateWorkflowPermissions } from '@/lib/workflows/utils'
|
||||
import { createErrorResponse, createSuccessResponse } from '@/app/api/workflows/utils'
|
||||
@@ -174,6 +175,14 @@ export async function PATCH(
|
||||
}
|
||||
}
|
||||
|
||||
const wsId = (workflowData as { workspaceId?: string } | null)?.workspaceId
|
||||
captureServerEvent(
|
||||
actorUserId,
|
||||
'deployment_version_activated',
|
||||
{ workflow_id: id, workspace_id: wsId ?? '', version: versionNum },
|
||||
wsId ? { groups: { workspace: wsId } } : undefined
|
||||
)
|
||||
|
||||
return createSuccessResponse({
|
||||
success: true,
|
||||
deployedAt: activateResult.deployedAt,
|
||||
|
||||
@@ -5,6 +5,7 @@ import { AuditAction, AuditResourceType, recordAudit } from '@/lib/audit/log'
|
||||
import { checkSessionOrInternalAuth } from '@/lib/auth/hybrid'
|
||||
import { PlatformEvents } from '@/lib/core/telemetry'
|
||||
import { generateRequestId } from '@/lib/core/utils/request'
|
||||
import { captureServerEvent } from '@/lib/posthog/server'
|
||||
import { duplicateWorkflow } from '@/lib/workflows/persistence/duplicate'
|
||||
|
||||
const logger = createLogger('WorkflowDuplicateAPI')
|
||||
@@ -60,6 +61,17 @@ export async function POST(req: NextRequest, { params }: { params: Promise<{ id:
|
||||
// Telemetry should not fail the operation
|
||||
}
|
||||
|
||||
captureServerEvent(
|
||||
userId,
|
||||
'workflow_duplicated',
|
||||
{
|
||||
source_workflow_id: sourceWorkflowId,
|
||||
new_workflow_id: result.id,
|
||||
workspace_id: workspaceId ?? '',
|
||||
},
|
||||
workspaceId ? { groups: { workspace: workspaceId } } : undefined
|
||||
)
|
||||
|
||||
const elapsed = Date.now() - startTime
|
||||
logger.info(
|
||||
`[${requestId}] Successfully duplicated workflow ${sourceWorkflowId} to ${result.id} in ${elapsed}ms`
|
||||
|
||||
@@ -3,6 +3,7 @@ import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { checkHybridAuth } from '@/lib/auth/hybrid'
|
||||
import { markExecutionCancelled } from '@/lib/execution/cancellation'
|
||||
import { abortManualExecution } from '@/lib/execution/manual-cancellation'
|
||||
import { captureServerEvent } from '@/lib/posthog/server'
|
||||
import { authorizeWorkflowByWorkspacePermission } from '@/lib/workflows/utils'
|
||||
|
||||
const logger = createLogger('CancelExecutionAPI')
|
||||
@@ -60,6 +61,16 @@ export async function POST(
|
||||
})
|
||||
}
|
||||
|
||||
if (cancellation.durablyRecorded || locallyAborted) {
|
||||
const workspaceId = workflowAuthorization.workflow?.workspaceId
|
||||
captureServerEvent(
|
||||
auth.userId,
|
||||
'workflow_execution_cancelled',
|
||||
{ workflow_id: workflowId, workspace_id: workspaceId ?? '' },
|
||||
workspaceId ? { groups: { workspace: workspaceId } } : undefined
|
||||
)
|
||||
}
|
||||
|
||||
return NextResponse.json({
|
||||
success: cancellation.durablyRecorded || locallyAborted,
|
||||
executionId,
|
||||
|
||||
@@ -3,6 +3,7 @@ import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { AuditAction, AuditResourceType, recordAudit } from '@/lib/audit/log'
|
||||
import { checkSessionOrInternalAuth } from '@/lib/auth/hybrid'
|
||||
import { generateRequestId } from '@/lib/core/utils/request'
|
||||
import { captureServerEvent } from '@/lib/posthog/server'
|
||||
import { restoreWorkflow } from '@/lib/workflows/lifecycle'
|
||||
import { getWorkflowById } from '@/lib/workflows/utils'
|
||||
import { getUserEntityPermissions } from '@/lib/workspaces/permissions/utils'
|
||||
@@ -58,6 +59,13 @@ export async function POST(request: NextRequest, { params }: { params: Promise<{
|
||||
request,
|
||||
})
|
||||
|
||||
captureServerEvent(
|
||||
auth.userId,
|
||||
'workflow_restored',
|
||||
{ workflow_id: workflowId, workspace_id: workflowData.workspaceId ?? '' },
|
||||
workflowData.workspaceId ? { groups: { workspace: workflowData.workspaceId } } : undefined
|
||||
)
|
||||
|
||||
return NextResponse.json({ success: true })
|
||||
} catch (error) {
|
||||
logger.error(`[${requestId}] Error restoring workflow ${workflowId}`, error)
|
||||
|
||||
@@ -6,6 +6,7 @@ import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { z } from 'zod'
|
||||
import { AuthType, checkHybridAuth, checkSessionOrInternalAuth } from '@/lib/auth/hybrid'
|
||||
import { generateRequestId } from '@/lib/core/utils/request'
|
||||
import { captureServerEvent } from '@/lib/posthog/server'
|
||||
import { performDeleteWorkflow } from '@/lib/workflows/orchestration'
|
||||
import { loadWorkflowFromNormalizedTables } from '@/lib/workflows/persistence/utils'
|
||||
import { authorizeWorkflowByWorkspacePermission, getWorkflowById } from '@/lib/workflows/utils'
|
||||
@@ -225,6 +226,13 @@ export async function DELETE(
|
||||
return NextResponse.json({ error: result.error }, { status })
|
||||
}
|
||||
|
||||
captureServerEvent(
|
||||
userId,
|
||||
'workflow_deleted',
|
||||
{ workflow_id: workflowId, workspace_id: workflowData.workspaceId ?? '' },
|
||||
workflowData.workspaceId ? { groups: { workspace: workflowData.workspaceId } } : undefined
|
||||
)
|
||||
|
||||
const elapsed = Date.now() - startTime
|
||||
logger.info(`[${requestId}] Successfully archived workflow ${workflowId} in ${elapsed}ms`)
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ import { z } from 'zod'
|
||||
import { AuditAction, AuditResourceType, recordAudit } from '@/lib/audit/log'
|
||||
import { checkSessionOrInternalAuth } from '@/lib/auth/hybrid'
|
||||
import { generateRequestId } from '@/lib/core/utils/request'
|
||||
import { captureServerEvent } from '@/lib/posthog/server'
|
||||
import { getNextWorkflowColor } from '@/lib/workflows/colors'
|
||||
import { buildDefaultWorkflowArtifacts } from '@/lib/workflows/defaults'
|
||||
import { saveWorkflowToNormalizedTables } from '@/lib/workflows/persistence/utils'
|
||||
@@ -274,6 +275,16 @@ export async function POST(req: NextRequest) {
|
||||
|
||||
logger.info(`[${requestId}] Successfully created workflow ${workflowId} with default blocks`)
|
||||
|
||||
captureServerEvent(
|
||||
userId,
|
||||
'workflow_created',
|
||||
{ workflow_id: workflowId, workspace_id: workspaceId ?? '', name },
|
||||
{
|
||||
groups: workspaceId ? { workspace: workspaceId } : undefined,
|
||||
setOnce: { first_workflow_created_at: new Date().toISOString() },
|
||||
}
|
||||
)
|
||||
|
||||
recordAudit({
|
||||
workspaceId,
|
||||
actorId: userId,
|
||||
|
||||
@@ -7,6 +7,7 @@ import { z } from 'zod'
|
||||
import { AuditAction, AuditResourceType, recordAudit } from '@/lib/audit/log'
|
||||
import { getSession } from '@/lib/auth'
|
||||
import { generateRequestId } from '@/lib/core/utils/request'
|
||||
import { captureServerEvent } from '@/lib/posthog/server'
|
||||
import { getUserEntityPermissions } from '@/lib/workspaces/permissions/utils'
|
||||
|
||||
const logger = createLogger('WorkspaceApiKeyAPI')
|
||||
@@ -145,6 +146,13 @@ export async function DELETE(
|
||||
|
||||
const deletedKey = deletedRows[0]
|
||||
|
||||
captureServerEvent(
|
||||
userId,
|
||||
'api_key_revoked',
|
||||
{ workspace_id: workspaceId, key_name: deletedKey.name },
|
||||
{ groups: { workspace: workspaceId } }
|
||||
)
|
||||
|
||||
recordAudit({
|
||||
workspaceId,
|
||||
actorId: userId,
|
||||
|
||||
@@ -10,12 +10,14 @@ import { AuditAction, AuditResourceType, recordAudit } from '@/lib/audit/log'
|
||||
import { getSession } from '@/lib/auth'
|
||||
import { PlatformEvents } from '@/lib/core/telemetry'
|
||||
import { generateRequestId } from '@/lib/core/utils/request'
|
||||
import { captureServerEvent } from '@/lib/posthog/server'
|
||||
import { getUserEntityPermissions, getWorkspaceById } from '@/lib/workspaces/permissions/utils'
|
||||
|
||||
const logger = createLogger('WorkspaceApiKeysAPI')
|
||||
|
||||
const CreateKeySchema = z.object({
|
||||
name: z.string().trim().min(1, 'Name is required'),
|
||||
source: z.enum(['settings', 'deploy_modal']).optional(),
|
||||
})
|
||||
|
||||
const DeleteKeysSchema = z.object({
|
||||
@@ -101,7 +103,7 @@ export async function POST(request: NextRequest, { params }: { params: Promise<{
|
||||
}
|
||||
|
||||
const body = await request.json()
|
||||
const { name } = CreateKeySchema.parse(body)
|
||||
const { name, source } = CreateKeySchema.parse(body)
|
||||
|
||||
const existingKey = await db
|
||||
.select()
|
||||
@@ -158,6 +160,16 @@ export async function POST(request: NextRequest, { params }: { params: Promise<{
|
||||
// Telemetry should not fail the operation
|
||||
}
|
||||
|
||||
captureServerEvent(
|
||||
userId,
|
||||
'api_key_created',
|
||||
{ workspace_id: workspaceId, key_name: name, source },
|
||||
{
|
||||
groups: { workspace: workspaceId },
|
||||
setOnce: { first_api_key_created_at: new Date().toISOString() },
|
||||
}
|
||||
)
|
||||
|
||||
logger.info(`[${requestId}] Created workspace API key: ${name} in workspace ${workspaceId}`)
|
||||
|
||||
recordAudit({
|
||||
|
||||
@@ -9,6 +9,7 @@ import { AuditAction, AuditResourceType, recordAudit } from '@/lib/audit/log'
|
||||
import { getSession } from '@/lib/auth'
|
||||
import { decryptSecret, encryptSecret } from '@/lib/core/security/encryption'
|
||||
import { generateRequestId } from '@/lib/core/utils/request'
|
||||
import { captureServerEvent } from '@/lib/posthog/server'
|
||||
import { getUserEntityPermissions, getWorkspaceById } from '@/lib/workspaces/permissions/utils'
|
||||
|
||||
const logger = createLogger('WorkspaceBYOKKeysAPI')
|
||||
@@ -201,6 +202,16 @@ export async function POST(request: NextRequest, { params }: { params: Promise<{
|
||||
|
||||
logger.info(`[${requestId}] Created BYOK key for ${providerId} in workspace ${workspaceId}`)
|
||||
|
||||
captureServerEvent(
|
||||
userId,
|
||||
'byok_key_added',
|
||||
{ workspace_id: workspaceId, provider_id: providerId },
|
||||
{
|
||||
groups: { workspace: workspaceId },
|
||||
setOnce: { first_byok_key_added_at: new Date().toISOString() },
|
||||
}
|
||||
)
|
||||
|
||||
recordAudit({
|
||||
workspaceId,
|
||||
actorId: userId,
|
||||
@@ -272,6 +283,13 @@ export async function DELETE(
|
||||
|
||||
logger.info(`[${requestId}] Deleted BYOK key for ${providerId} from workspace ${workspaceId}`)
|
||||
|
||||
captureServerEvent(
|
||||
userId,
|
||||
'byok_key_removed',
|
||||
{ workspace_id: workspaceId, provider_id: providerId },
|
||||
{ groups: { workspace: workspaceId } }
|
||||
)
|
||||
|
||||
recordAudit({
|
||||
workspaceId,
|
||||
actorId: userId,
|
||||
|
||||
@@ -3,6 +3,7 @@ import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { AuditAction, AuditResourceType, recordAudit } from '@/lib/audit/log'
|
||||
import { getSession } from '@/lib/auth'
|
||||
import { generateRequestId } from '@/lib/core/utils/request'
|
||||
import { captureServerEvent } from '@/lib/posthog/server'
|
||||
import {
|
||||
FileConflictError,
|
||||
listWorkspaceFiles,
|
||||
@@ -116,6 +117,13 @@ export async function POST(request: NextRequest, { params }: { params: Promise<{
|
||||
|
||||
logger.info(`[${requestId}] Uploaded workspace file: ${fileName}`)
|
||||
|
||||
captureServerEvent(
|
||||
session.user.id,
|
||||
'file_uploaded',
|
||||
{ workspace_id: workspaceId, file_type: rawFile.type || 'application/octet-stream' },
|
||||
{ groups: { workspace: workspaceId } }
|
||||
)
|
||||
|
||||
recordAudit({
|
||||
workspaceId,
|
||||
actorId: session.user.id,
|
||||
|
||||
@@ -7,6 +7,7 @@ import { z } from 'zod'
|
||||
import { AuditAction, AuditResourceType, recordAudit } from '@/lib/audit/log'
|
||||
import { getSession } from '@/lib/auth'
|
||||
import { encryptSecret } from '@/lib/core/security/encryption'
|
||||
import { captureServerEvent } from '@/lib/posthog/server'
|
||||
import { getUserEntityPermissions } from '@/lib/workspaces/permissions/utils'
|
||||
import { MAX_EMAIL_RECIPIENTS, MAX_WORKFLOW_IDS } from '../constants'
|
||||
|
||||
@@ -342,6 +343,17 @@ export async function DELETE(request: NextRequest, { params }: RouteParams) {
|
||||
request,
|
||||
})
|
||||
|
||||
captureServerEvent(
|
||||
session.user.id,
|
||||
'notification_channel_deleted',
|
||||
{
|
||||
notification_id: notificationId,
|
||||
notification_type: deletedSubscription.notificationType,
|
||||
workspace_id: workspaceId,
|
||||
},
|
||||
{ groups: { workspace: workspaceId } }
|
||||
)
|
||||
|
||||
return NextResponse.json({ success: true })
|
||||
} catch (error) {
|
||||
logger.error('Error deleting notification', { error })
|
||||
|
||||
@@ -8,6 +8,7 @@ import { z } from 'zod'
|
||||
import { AuditAction, AuditResourceType, recordAudit } from '@/lib/audit/log'
|
||||
import { getSession } from '@/lib/auth'
|
||||
import { encryptSecret } from '@/lib/core/security/encryption'
|
||||
import { captureServerEvent } from '@/lib/posthog/server'
|
||||
import { getUserEntityPermissions } from '@/lib/workspaces/permissions/utils'
|
||||
import { MAX_EMAIL_RECIPIENTS, MAX_NOTIFICATIONS_PER_TYPE, MAX_WORKFLOW_IDS } from './constants'
|
||||
|
||||
@@ -256,6 +257,17 @@ export async function POST(request: NextRequest, { params }: { params: Promise<{
|
||||
type: data.notificationType,
|
||||
})
|
||||
|
||||
captureServerEvent(
|
||||
session.user.id,
|
||||
'notification_channel_created',
|
||||
{
|
||||
workspace_id: workspaceId,
|
||||
notification_type: data.notificationType,
|
||||
alert_rule: data.alertConfig?.rule ?? null,
|
||||
},
|
||||
{ groups: { workspace: workspaceId } }
|
||||
)
|
||||
|
||||
recordAudit({
|
||||
workspaceId,
|
||||
actorId: session.user.id,
|
||||
|
||||
@@ -8,6 +8,7 @@ import { z } from 'zod'
|
||||
import { AuditAction, AuditResourceType, recordAudit } from '@/lib/audit/log'
|
||||
import { getSession } from '@/lib/auth'
|
||||
import { syncWorkspaceEnvCredentials } from '@/lib/credentials/environment'
|
||||
import { captureServerEvent } from '@/lib/posthog/server'
|
||||
import {
|
||||
getUsersWithPermissions,
|
||||
hasWorkspaceAdminAccess,
|
||||
@@ -188,6 +189,13 @@ export async function PATCH(request: NextRequest, { params }: { params: Promise<
|
||||
const updatedUsers = await getUsersWithPermissions(workspaceId)
|
||||
|
||||
for (const update of body.updates) {
|
||||
captureServerEvent(
|
||||
session.user.id,
|
||||
'workspace_member_role_changed',
|
||||
{ workspace_id: workspaceId, new_role: update.permissions },
|
||||
{ groups: { workspace: workspaceId } }
|
||||
)
|
||||
|
||||
recordAudit({
|
||||
workspaceId,
|
||||
actorId: session.user.id,
|
||||
|
||||
@@ -5,6 +5,7 @@ import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { z } from 'zod'
|
||||
import { AuditAction, AuditResourceType, recordAudit } from '@/lib/audit/log'
|
||||
import { getSession } from '@/lib/auth'
|
||||
import { captureServerEvent } from '@/lib/posthog/server'
|
||||
import { archiveWorkspace } from '@/lib/workspaces/lifecycle'
|
||||
|
||||
const logger = createLogger('WorkspaceByIdAPI')
|
||||
@@ -292,6 +293,13 @@ export async function DELETE(
|
||||
request,
|
||||
})
|
||||
|
||||
captureServerEvent(
|
||||
session.user.id,
|
||||
'workspace_deleted',
|
||||
{ workspace_id: workspaceId, workflow_count: workflowIds.length },
|
||||
{ groups: { workspace: workspaceId } }
|
||||
)
|
||||
|
||||
return NextResponse.json({ success: true })
|
||||
} catch (error) {
|
||||
logger.error(`Error deleting workspace ${workspaceId}:`, error)
|
||||
|
||||
@@ -19,6 +19,7 @@ import { PlatformEvents } from '@/lib/core/telemetry'
|
||||
import { getBaseUrl } from '@/lib/core/utils/urls'
|
||||
import { sendEmail } from '@/lib/messaging/email/mailer'
|
||||
import { getFromEmailAddress } from '@/lib/messaging/email/utils'
|
||||
import { captureServerEvent } from '@/lib/posthog/server'
|
||||
import { getWorkspaceById } from '@/lib/workspaces/permissions/utils'
|
||||
import {
|
||||
InvitationsNotAllowedError,
|
||||
@@ -214,6 +215,16 @@ export async function POST(req: NextRequest) {
|
||||
// Telemetry should not fail the operation
|
||||
}
|
||||
|
||||
captureServerEvent(
|
||||
session.user.id,
|
||||
'workspace_member_invited',
|
||||
{ workspace_id: workspaceId, invitee_role: permission },
|
||||
{
|
||||
groups: { workspace: workspaceId },
|
||||
setOnce: { first_invitation_sent_at: new Date().toISOString() },
|
||||
}
|
||||
)
|
||||
|
||||
await sendInvitationEmail({
|
||||
to: email,
|
||||
inviterName: session.user.name || session.user.email || 'A user',
|
||||
|
||||
@@ -7,6 +7,7 @@ import { z } from 'zod'
|
||||
import { AuditAction, AuditResourceType, recordAudit } from '@/lib/audit/log'
|
||||
import { getSession } from '@/lib/auth'
|
||||
import { revokeWorkspaceCredentialMemberships } from '@/lib/credentials/access'
|
||||
import { captureServerEvent } from '@/lib/posthog/server'
|
||||
import { hasWorkspaceAdminAccess } from '@/lib/workspaces/permissions/utils'
|
||||
|
||||
const logger = createLogger('WorkspaceMemberAPI')
|
||||
@@ -105,6 +106,13 @@ export async function DELETE(req: NextRequest, { params }: { params: Promise<{ i
|
||||
|
||||
await revokeWorkspaceCredentialMemberships(workspaceId, userId)
|
||||
|
||||
captureServerEvent(
|
||||
session.user.id,
|
||||
'workspace_member_removed',
|
||||
{ workspace_id: workspaceId, is_self_removal: isSelf },
|
||||
{ groups: { workspace: workspaceId } }
|
||||
)
|
||||
|
||||
recordAudit({
|
||||
workspaceId,
|
||||
actorId: session.user.id,
|
||||
|
||||
@@ -7,6 +7,7 @@ import { z } from 'zod'
|
||||
import { AuditAction, AuditResourceType, recordAudit } from '@/lib/audit/log'
|
||||
import { getSession } from '@/lib/auth'
|
||||
import { PlatformEvents } from '@/lib/core/telemetry'
|
||||
import { captureServerEvent } from '@/lib/posthog/server'
|
||||
import { buildDefaultWorkflowArtifacts } from '@/lib/workflows/defaults'
|
||||
import { saveWorkflowToNormalizedTables } from '@/lib/workflows/persistence/utils'
|
||||
import { getRandomWorkspaceColor } from '@/lib/workspaces/colors'
|
||||
@@ -96,6 +97,16 @@ export async function POST(req: Request) {
|
||||
|
||||
const newWorkspace = await createWorkspace(session.user.id, name, skipDefaultWorkflow, color)
|
||||
|
||||
captureServerEvent(
|
||||
session.user.id,
|
||||
'workspace_created',
|
||||
{ workspace_id: newWorkspace.id, name: newWorkspace.name },
|
||||
{
|
||||
groups: { workspace: newWorkspace.id },
|
||||
setOnce: { first_workspace_created_at: new Date().toISOString() },
|
||||
}
|
||||
)
|
||||
|
||||
recordAudit({
|
||||
workspaceId: newWorkspace.id,
|
||||
actorId: session.user.id,
|
||||
|
||||
@@ -26,6 +26,7 @@ export function NavTour() {
|
||||
steps: navTourSteps,
|
||||
triggerEvent: START_NAV_TOUR_EVENT,
|
||||
tourName: 'Navigation tour',
|
||||
tourType: 'nav',
|
||||
disabled: isWorkflowPage,
|
||||
})
|
||||
|
||||
|
||||
@@ -2,7 +2,9 @@
|
||||
|
||||
import { useCallback, useEffect, useRef, useState } from 'react'
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { usePostHog } from 'posthog-js/react'
|
||||
import { ACTIONS, type CallBackProps, EVENTS, STATUS, type Step } from 'react-joyride'
|
||||
import { captureEvent } from '@/lib/posthog/client'
|
||||
|
||||
const logger = createLogger('useTour')
|
||||
|
||||
@@ -16,6 +18,8 @@ interface UseTourOptions {
|
||||
triggerEvent?: string
|
||||
/** Identifier for logging */
|
||||
tourName?: string
|
||||
/** Analytics tour type for PostHog events */
|
||||
tourType?: 'nav' | 'workflow'
|
||||
/** When true, stops a running tour (e.g. navigating away from the relevant page) */
|
||||
disabled?: boolean
|
||||
}
|
||||
@@ -45,8 +49,10 @@ export function useTour({
|
||||
steps,
|
||||
triggerEvent,
|
||||
tourName = 'tour',
|
||||
tourType,
|
||||
disabled = false,
|
||||
}: UseTourOptions): UseTourReturn {
|
||||
const posthog = usePostHog()
|
||||
const [run, setRun] = useState(false)
|
||||
const [stepIndex, setStepIndex] = useState(0)
|
||||
const [tourKey, setTourKey] = useState(0)
|
||||
@@ -152,6 +158,9 @@ export function useTour({
|
||||
setRun(true)
|
||||
logger.info(`${tourName} triggered via event`)
|
||||
scheduleReveal()
|
||||
if (tourType) {
|
||||
captureEvent(posthog, 'tour_started', { tour_type: tourType })
|
||||
}
|
||||
}, 50)
|
||||
}
|
||||
|
||||
@@ -181,6 +190,13 @@ export function useTour({
|
||||
if (status === STATUS.FINISHED || status === STATUS.SKIPPED) {
|
||||
stopTour()
|
||||
logger.info(`${tourName} ended`, { status })
|
||||
if (tourType) {
|
||||
if (status === STATUS.FINISHED) {
|
||||
captureEvent(posthog, 'tour_completed', { tour_type: tourType })
|
||||
} else {
|
||||
captureEvent(posthog, 'tour_skipped', { tour_type: tourType, step_index: index })
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
@@ -188,6 +204,9 @@ export function useTour({
|
||||
if (action === ACTIONS.CLOSE) {
|
||||
stopTour()
|
||||
logger.info(`${tourName} closed by user`)
|
||||
if (tourType) {
|
||||
captureEvent(posthog, 'tour_skipped', { tour_type: tourType, step_index: index })
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
@@ -203,7 +222,7 @@ export function useTour({
|
||||
transitionToStep(nextIndex)
|
||||
}
|
||||
},
|
||||
[stopTour, transitionToStep, steps, tourName]
|
||||
[stopTour, transitionToStep, steps, tourName, tourType, posthog]
|
||||
)
|
||||
|
||||
return {
|
||||
|
||||
@@ -26,6 +26,7 @@ export function WorkflowTour() {
|
||||
steps: workflowTourSteps,
|
||||
triggerEvent: START_WORKFLOW_TOUR_EVENT,
|
||||
tourName: 'Workflow tour',
|
||||
tourType: 'workflow',
|
||||
})
|
||||
|
||||
const tourState = useMemo<TourState>(
|
||||
|
||||
@@ -353,7 +353,17 @@ const TemplateCard = memo(function TemplateCard({ template, onSelect }: Template
|
||||
return (
|
||||
<button
|
||||
type='button'
|
||||
onClick={() => onSelect(template.prompt)}
|
||||
onClick={() => {
|
||||
import('@/lib/posthog/client')
|
||||
.then(({ captureClientEvent }) => {
|
||||
captureClientEvent('template_used', {
|
||||
template_title: template.title,
|
||||
template_modules: template.modules.join(' '),
|
||||
})
|
||||
})
|
||||
.catch(() => {})
|
||||
onSelect(template.prompt)
|
||||
}}
|
||||
aria-label={`Select template: ${template.title}`}
|
||||
className='group flex cursor-pointer flex-col text-left'
|
||||
>
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
import { useCallback, useEffect, useRef, useState } from 'react'
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { useParams, useRouter } from 'next/navigation'
|
||||
import { usePostHog } from 'posthog-js/react'
|
||||
import { PanelLeft } from '@/components/emcn/icons'
|
||||
import { useSession } from '@/lib/auth/auth-client'
|
||||
import {
|
||||
@@ -10,6 +11,7 @@ import {
|
||||
type LandingWorkflowSeed,
|
||||
LandingWorkflowSeedStorage,
|
||||
} from '@/lib/core/utils/browser-storage'
|
||||
import { captureEvent } from '@/lib/posthog/client'
|
||||
import { persistImportedWorkflow } from '@/lib/workflows/operations/import-export'
|
||||
import { useChatHistory, useMarkTaskRead } from '@/hooks/queries/tasks'
|
||||
import type { ChatContext } from '@/stores/panel'
|
||||
@@ -27,6 +29,8 @@ export function Home({ chatId }: HomeProps = {}) {
|
||||
const { workspaceId } = useParams<{ workspaceId: string }>()
|
||||
const router = useRouter()
|
||||
const { data: session } = useSession()
|
||||
const posthog = usePostHog()
|
||||
const posthogRef = useRef(posthog)
|
||||
const [initialPrompt, setInitialPrompt] = useState('')
|
||||
const hasCheckedLandingStorageRef = useRef(false)
|
||||
const initialViewInputRef = useRef<HTMLDivElement>(null)
|
||||
@@ -199,11 +203,21 @@ export function Home({ chatId }: HomeProps = {}) {
|
||||
return () => cancelAnimationFrame(id)
|
||||
}, [resources])
|
||||
|
||||
useEffect(() => {
|
||||
posthogRef.current = posthog
|
||||
}, [posthog])
|
||||
|
||||
const handleSubmit = useCallback(
|
||||
(text: string, fileAttachments?: FileAttachmentForApi[], contexts?: ChatContext[]) => {
|
||||
const trimmed = text.trim()
|
||||
if (!trimmed && !(fileAttachments && fileAttachments.length > 0)) return
|
||||
|
||||
captureEvent(posthogRef.current, 'task_message_sent', {
|
||||
has_attachments: !!(fileAttachments && fileAttachments.length > 0),
|
||||
has_contexts: !!(contexts && contexts.length > 0),
|
||||
is_new_task: !chatId,
|
||||
})
|
||||
|
||||
if (initialViewInputRef.current) {
|
||||
setIsInputEntering(true)
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import { createLogger } from '@sim/logger'
|
||||
import { format } from 'date-fns'
|
||||
import { AlertCircle, Loader2, Pencil, Plus, Tag, X } from 'lucide-react'
|
||||
import { useParams, useRouter } from 'next/navigation'
|
||||
import { usePostHog } from 'posthog-js/react'
|
||||
import {
|
||||
Badge,
|
||||
Button,
|
||||
@@ -28,6 +29,7 @@ import { ALL_TAG_SLOTS, type AllTagSlot, getFieldTypeForSlot } from '@/lib/knowl
|
||||
import type { DocumentSortField, SortOrder } from '@/lib/knowledge/documents/types'
|
||||
import { type FilterFieldType, getOperatorsForFieldType } from '@/lib/knowledge/filters/types'
|
||||
import type { DocumentData } from '@/lib/knowledge/types'
|
||||
import { captureEvent } from '@/lib/posthog/client'
|
||||
import { formatFileSize } from '@/lib/uploads/utils/file-utils'
|
||||
import type {
|
||||
BreadcrumbItem,
|
||||
@@ -190,6 +192,15 @@ export function KnowledgeBase({
|
||||
}: KnowledgeBaseProps) {
|
||||
const params = useParams()
|
||||
const workspaceId = propWorkspaceId || (params.workspaceId as string)
|
||||
const posthog = usePostHog()
|
||||
|
||||
useEffect(() => {
|
||||
captureEvent(posthog, 'knowledge_base_opened', {
|
||||
knowledge_base_id: id,
|
||||
knowledge_base_name: passedKnowledgeBaseName ?? 'Unknown',
|
||||
})
|
||||
}, [id, passedKnowledgeBaseName, posthog])
|
||||
|
||||
useOAuthReturnForKBConnectors(id)
|
||||
const { removeKnowledgeBase } = useKnowledgeBasesList(workspaceId, { enabled: false })
|
||||
const userPermissions = useUserPermissionsContext()
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
import { useEffect } from 'react'
|
||||
import { useParams } from 'next/navigation'
|
||||
import { usePostHog } from 'posthog-js/react'
|
||||
import { useWorkflowRegistry } from '@/stores/workflows/registry/store'
|
||||
|
||||
/**
|
||||
@@ -11,6 +12,12 @@ export function WorkspaceScopeSync() {
|
||||
const { workspaceId } = useParams<{ workspaceId: string }>()
|
||||
const hydrationWorkspaceId = useWorkflowRegistry((state) => state.hydration.workspaceId)
|
||||
const switchToWorkspace = useWorkflowRegistry((state) => state.switchToWorkspace)
|
||||
const posthog = usePostHog()
|
||||
|
||||
useEffect(() => {
|
||||
if (!workspaceId) return
|
||||
posthog?.group('workspace', workspaceId)
|
||||
}, [posthog, workspaceId])
|
||||
|
||||
useEffect(() => {
|
||||
if (!workspaceId || hydrationWorkspaceId === workspaceId) {
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
'use client'
|
||||
|
||||
import { useEffect } from 'react'
|
||||
import dynamic from 'next/dynamic'
|
||||
import { useSearchParams } from 'next/navigation'
|
||||
import { usePostHog } from 'posthog-js/react'
|
||||
import { Skeleton } from '@/components/emcn'
|
||||
import { useSession } from '@/lib/auth/auth-client'
|
||||
import { captureEvent } from '@/lib/posthog/client'
|
||||
import { AdminSkeleton } from '@/app/workspace/[workspaceId]/settings/components/admin/admin-skeleton'
|
||||
import { ApiKeysSkeleton } from '@/app/workspace/[workspaceId]/settings/components/api-keys/api-key-skeleton'
|
||||
import { BYOKSkeleton } from '@/app/workspace/[workspaceId]/settings/components/byok/byok-skeleton'
|
||||
@@ -160,6 +163,7 @@ export function SettingsPage({ section }: SettingsPageProps) {
|
||||
const searchParams = useSearchParams()
|
||||
const mcpServerId = searchParams.get('mcpServerId')
|
||||
const { data: session, isPending: sessionLoading } = useSession()
|
||||
const posthog = usePostHog()
|
||||
|
||||
const isAdminRole = session?.user?.role === 'admin'
|
||||
const effectiveSection =
|
||||
@@ -174,6 +178,11 @@ export function SettingsPage({ section }: SettingsPageProps) {
|
||||
const label =
|
||||
allNavigationItems.find((item) => item.id === effectiveSection)?.label ?? effectiveSection
|
||||
|
||||
useEffect(() => {
|
||||
if (sessionLoading) return
|
||||
captureEvent(posthog, 'settings_tab_viewed', { section: effectiveSection })
|
||||
}, [effectiveSection, sessionLoading, posthog])
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h2 className='mb-7 font-medium text-[22px] text-[var(--text-primary)]'>{label}</h2>
|
||||
|
||||
@@ -26,6 +26,7 @@ interface CreateApiKeyModalProps {
|
||||
allowPersonalApiKeys?: boolean
|
||||
canManageWorkspaceKeys?: boolean
|
||||
defaultKeyType?: 'personal' | 'workspace'
|
||||
source?: 'settings' | 'deploy_modal'
|
||||
onKeyCreated?: (key: ApiKey) => void
|
||||
}
|
||||
|
||||
@@ -41,6 +42,7 @@ export function CreateApiKeyModal({
|
||||
allowPersonalApiKeys = true,
|
||||
canManageWorkspaceKeys = false,
|
||||
defaultKeyType = 'personal',
|
||||
source = 'settings',
|
||||
onKeyCreated,
|
||||
}: CreateApiKeyModalProps) {
|
||||
const [keyName, setKeyName] = useState('')
|
||||
@@ -74,6 +76,7 @@ export function CreateApiKeyModal({
|
||||
workspaceId,
|
||||
name: trimmedName,
|
||||
keyType,
|
||||
source,
|
||||
})
|
||||
|
||||
setNewKey(data.key)
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { GripVertical } from 'lucide-react'
|
||||
import { useParams, useRouter } from 'next/navigation'
|
||||
import { usePostHog } from 'posthog-js/react'
|
||||
import {
|
||||
Button,
|
||||
Checkbox,
|
||||
@@ -39,6 +40,7 @@ import {
|
||||
TypeText,
|
||||
} from '@/components/emcn/icons'
|
||||
import { cn } from '@/lib/core/utils/cn'
|
||||
import { captureEvent } from '@/lib/posthog/client'
|
||||
import type { ColumnDefinition, Filter, SortDirection, TableRow as TableRowType } from '@/lib/table'
|
||||
import type { ColumnOption, SortConfig } from '@/app/workspace/[workspaceId]/components'
|
||||
import { ResourceHeader, ResourceOptionsBar } from '@/app/workspace/[workspaceId]/components'
|
||||
@@ -177,6 +179,12 @@ export function Table({
|
||||
const router = useRouter()
|
||||
const workspaceId = propWorkspaceId || (params.workspaceId as string)
|
||||
const tableId = propTableId || (params.tableId as string)
|
||||
const posthog = usePostHog()
|
||||
|
||||
useEffect(() => {
|
||||
if (!tableId || !workspaceId) return
|
||||
captureEvent(posthog, 'table_opened', { table_id: tableId, workspace_id: workspaceId })
|
||||
}, [tableId, workspaceId, posthog])
|
||||
|
||||
const [queryOptions, setQueryOptions] = useState<QueryOptions>({
|
||||
filter: null,
|
||||
|
||||
@@ -915,6 +915,7 @@ export function DeployModal({
|
||||
allowPersonalApiKeys={allowPersonalApiKeys}
|
||||
canManageWorkspaceKeys={canManageWorkspaceKeys}
|
||||
defaultKeyType={defaultKeyType}
|
||||
source='deploy_modal'
|
||||
/>
|
||||
|
||||
{workflowId && (
|
||||
|
||||
@@ -59,14 +59,10 @@ interface ComboBoxProps {
|
||||
/** Configuration for the sub-block */
|
||||
config: SubBlockConfig
|
||||
/** Async function to fetch options dynamically */
|
||||
fetchOptions?: (
|
||||
blockId: string,
|
||||
subBlockId: string
|
||||
) => Promise<Array<{ label: string; id: string }>>
|
||||
fetchOptions?: (blockId: string) => Promise<Array<{ label: string; id: string }>>
|
||||
/** Async function to fetch a single option's label by ID (for hydration) */
|
||||
fetchOptionById?: (
|
||||
blockId: string,
|
||||
subBlockId: string,
|
||||
optionId: string
|
||||
) => Promise<{ label: string; id: string } | null>
|
||||
/** Field dependencies that trigger option refetch when changed */
|
||||
@@ -135,7 +131,7 @@ export const ComboBox = memo(function ComboBox({
|
||||
setIsLoadingOptions(true)
|
||||
setFetchError(null)
|
||||
try {
|
||||
const options = await fetchOptions(blockId, subBlockId)
|
||||
const options = await fetchOptions(blockId)
|
||||
setFetchedOptions(options)
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : 'Failed to fetch options'
|
||||
@@ -144,7 +140,7 @@ export const ComboBox = memo(function ComboBox({
|
||||
} finally {
|
||||
setIsLoadingOptions(false)
|
||||
}
|
||||
}, [fetchOptions, blockId, subBlockId, isPreview, disabled])
|
||||
}, [fetchOptions, blockId, isPreview, disabled])
|
||||
|
||||
// Determine the active value based on mode (preview vs. controlled vs. store)
|
||||
const value = isPreview ? previewValue : propValue !== undefined ? propValue : storeValue
|
||||
@@ -363,7 +359,7 @@ export const ComboBox = memo(function ComboBox({
|
||||
let isActive = true
|
||||
|
||||
// Fetch the hydrated option
|
||||
fetchOptionById(blockId, subBlockId, valueToHydrate)
|
||||
fetchOptionById(blockId, valueToHydrate)
|
||||
.then((option) => {
|
||||
if (isActive) setHydratedOption(option)
|
||||
})
|
||||
@@ -378,7 +374,6 @@ export const ComboBox = memo(function ComboBox({
|
||||
fetchOptionById,
|
||||
value,
|
||||
blockId,
|
||||
subBlockId,
|
||||
isPreview,
|
||||
disabled,
|
||||
fetchedOptions,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
'use client'
|
||||
|
||||
import { createElement, useCallback, useMemo, useState } from 'react'
|
||||
import { ExternalLink, Users } from 'lucide-react'
|
||||
import { ExternalLink, KeyRound, Users } from 'lucide-react'
|
||||
import { useParams } from 'next/navigation'
|
||||
import { Button, Combobox } from '@/components/emcn/components'
|
||||
import { getSubscriptionAccessState } from '@/lib/billing/client'
|
||||
@@ -22,7 +22,7 @@ import { useSubBlockValue } from '@/app/workspace/[workspaceId]/w/[workflowId]/c
|
||||
import type { SubBlockConfig } from '@/blocks/types'
|
||||
import { CREDENTIAL_SET } from '@/executor/constants'
|
||||
import { useCredentialSets } from '@/hooks/queries/credential-sets'
|
||||
import { useWorkspaceCredential } from '@/hooks/queries/credentials'
|
||||
import { useWorkspaceCredential, useWorkspaceCredentials } from '@/hooks/queries/credentials'
|
||||
import { useOAuthCredentials } from '@/hooks/queries/oauth/oauth-credentials'
|
||||
import { useOrganizations } from '@/hooks/queries/organization'
|
||||
import { useSubscriptionData } from '@/hooks/queries/subscription'
|
||||
@@ -60,6 +60,7 @@ export function CredentialSelector({
|
||||
const requiredScopes = subBlock.requiredScopes || []
|
||||
const label = subBlock.placeholder || 'Select credential'
|
||||
const serviceId = subBlock.serviceId || ''
|
||||
const isAllCredentials = !serviceId
|
||||
const supportsCredentialSets = subBlock.supportsCredentialSets || false
|
||||
|
||||
const { data: organizationsData } = useOrganizations()
|
||||
@@ -101,14 +102,22 @@ export function CredentialSelector({
|
||||
|
||||
const {
|
||||
data: rawCredentials = [],
|
||||
isFetching: credentialsLoading,
|
||||
isFetching: oauthCredentialsLoading,
|
||||
refetch: refetchCredentials,
|
||||
} = useOAuthCredentials(effectiveProviderId, {
|
||||
enabled: Boolean(effectiveProviderId),
|
||||
enabled: !isAllCredentials && Boolean(effectiveProviderId),
|
||||
workspaceId,
|
||||
workflowId: activeWorkflowId || undefined,
|
||||
})
|
||||
|
||||
const {
|
||||
data: allWorkspaceCredentials = [],
|
||||
isFetching: allCredentialsLoading,
|
||||
refetch: refetchAllCredentials,
|
||||
} = useWorkspaceCredentials({ workspaceId, enabled: isAllCredentials })
|
||||
|
||||
const credentialsLoading = isAllCredentials ? allCredentialsLoading : oauthCredentialsLoading
|
||||
|
||||
const credentials = useMemo(
|
||||
() =>
|
||||
isTriggerMode
|
||||
@@ -122,9 +131,17 @@ export function CredentialSelector({
|
||||
[credentials, selectedId]
|
||||
)
|
||||
|
||||
const selectedAllCredential = useMemo(
|
||||
() =>
|
||||
isAllCredentials ? (allWorkspaceCredentials.find((c) => c.id === selectedId) ?? null) : null,
|
||||
[isAllCredentials, allWorkspaceCredentials, selectedId]
|
||||
)
|
||||
|
||||
const isServiceAccount = useMemo(
|
||||
() => selectedCredential?.type === 'service_account',
|
||||
[selectedCredential]
|
||||
() =>
|
||||
selectedCredential?.type === 'service_account' ||
|
||||
selectedAllCredential?.type === 'service_account',
|
||||
[selectedCredential, selectedAllCredential]
|
||||
)
|
||||
|
||||
const selectedCredentialSet = useMemo(
|
||||
@@ -134,37 +151,45 @@ export function CredentialSelector({
|
||||
|
||||
const { data: inaccessibleCredential } = useWorkspaceCredential(
|
||||
selectedId || undefined,
|
||||
Boolean(selectedId) && !selectedCredential && !credentialsLoading && Boolean(workspaceId)
|
||||
Boolean(selectedId) &&
|
||||
!selectedCredential &&
|
||||
!selectedAllCredential &&
|
||||
!credentialsLoading &&
|
||||
Boolean(workspaceId)
|
||||
)
|
||||
const inaccessibleCredentialName = inaccessibleCredential?.displayName ?? null
|
||||
|
||||
const resolvedLabel = useMemo(() => {
|
||||
if (selectedCredentialSet) return selectedCredentialSet.name
|
||||
if (selectedAllCredential) return selectedAllCredential.displayName
|
||||
if (selectedCredential) return selectedCredential.name
|
||||
if (inaccessibleCredentialName) return inaccessibleCredentialName
|
||||
return ''
|
||||
}, [selectedCredentialSet, selectedCredential, inaccessibleCredentialName])
|
||||
}, [selectedCredentialSet, selectedAllCredential, selectedCredential, inaccessibleCredentialName])
|
||||
|
||||
const displayValue = isEditing ? editingValue : resolvedLabel
|
||||
|
||||
useCredentialRefreshTriggers(refetchCredentials, effectiveProviderId, workspaceId)
|
||||
const refetch = useCallback(
|
||||
() => (isAllCredentials ? refetchAllCredentials() : refetchCredentials()),
|
||||
[isAllCredentials, refetchAllCredentials, refetchCredentials]
|
||||
)
|
||||
|
||||
useCredentialRefreshTriggers(refetch, effectiveProviderId, workspaceId)
|
||||
|
||||
const handleOpenChange = useCallback(
|
||||
(isOpen: boolean) => {
|
||||
if (isOpen) {
|
||||
void refetchCredentials()
|
||||
}
|
||||
if (isOpen) void refetch()
|
||||
},
|
||||
[refetchCredentials]
|
||||
[refetch]
|
||||
)
|
||||
|
||||
const hasSelection = Boolean(selectedCredential)
|
||||
const missingRequiredScopes = hasSelection
|
||||
const hasOAuthSelection = Boolean(selectedCredential)
|
||||
const missingRequiredScopes = hasOAuthSelection
|
||||
? getMissingRequiredScopes(selectedCredential!, requiredScopes || [])
|
||||
: []
|
||||
|
||||
const needsUpdate =
|
||||
hasSelection &&
|
||||
hasOAuthSelection &&
|
||||
!isServiceAccount &&
|
||||
missingRequiredScopes.length > 0 &&
|
||||
!effectiveDisabled &&
|
||||
@@ -218,6 +243,12 @@ export function CredentialSelector({
|
||||
}, [])
|
||||
|
||||
const { comboboxOptions, comboboxGroups } = useMemo(() => {
|
||||
if (isAllCredentials) {
|
||||
const oauthCredentials = allWorkspaceCredentials.filter((c) => c.type === 'oauth')
|
||||
const options = oauthCredentials.map((cred) => ({ label: cred.displayName, value: cred.id }))
|
||||
return { comboboxOptions: options, comboboxGroups: undefined }
|
||||
}
|
||||
|
||||
const pollingProviderId = getPollingProviderFromOAuth(effectiveProviderId)
|
||||
// Handle both old ('gmail') and new ('google-email') provider IDs for backwards compatibility
|
||||
const matchesProvider = (csProviderId: string | null) => {
|
||||
@@ -281,6 +312,8 @@ export function CredentialSelector({
|
||||
|
||||
return { comboboxOptions: options, comboboxGroups: undefined }
|
||||
}, [
|
||||
isAllCredentials,
|
||||
allWorkspaceCredentials,
|
||||
credentials,
|
||||
provider,
|
||||
effectiveProviderId,
|
||||
@@ -306,6 +339,17 @@ export function CredentialSelector({
|
||||
)
|
||||
}
|
||||
|
||||
if (isAllCredentials && selectedAllCredential) {
|
||||
return (
|
||||
<div className='flex w-full items-center truncate'>
|
||||
<div className='mr-2 flex-shrink-0 opacity-90'>
|
||||
<KeyRound className='h-3 w-3' />
|
||||
</div>
|
||||
<span className='truncate'>{displayValue}</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='flex w-full items-center truncate'>
|
||||
<div className='mr-2 flex-shrink-0 opacity-90'>
|
||||
@@ -320,7 +364,8 @@ export function CredentialSelector({
|
||||
selectedCredentialProvider,
|
||||
isCredentialSetSelected,
|
||||
selectedCredentialSet,
|
||||
isServiceAccount,
|
||||
isAllCredentials,
|
||||
selectedAllCredential,
|
||||
])
|
||||
|
||||
const handleComboboxChange = useCallback(
|
||||
@@ -339,7 +384,9 @@ export function CredentialSelector({
|
||||
}
|
||||
}
|
||||
|
||||
const matchedCred = credentials.find((c) => c.id === value)
|
||||
const matchedCred = (
|
||||
isAllCredentials ? allWorkspaceCredentials.filter((c) => c.type === 'oauth') : credentials
|
||||
).find((c) => c.id === value)
|
||||
if (matchedCred) {
|
||||
handleSelect(value)
|
||||
return
|
||||
@@ -348,7 +395,15 @@ export function CredentialSelector({
|
||||
setIsEditing(true)
|
||||
setEditingValue(value)
|
||||
},
|
||||
[credentials, credentialSets, handleAddCredential, handleSelect, handleCredentialSetSelect]
|
||||
[
|
||||
isAllCredentials,
|
||||
allWorkspaceCredentials,
|
||||
credentials,
|
||||
credentialSets,
|
||||
handleAddCredential,
|
||||
handleSelect,
|
||||
handleCredentialSetSelect,
|
||||
]
|
||||
)
|
||||
|
||||
return (
|
||||
|
||||
@@ -52,14 +52,10 @@ interface DropdownProps {
|
||||
/** Enable multi-select mode */
|
||||
multiSelect?: boolean
|
||||
/** Async function to fetch options dynamically */
|
||||
fetchOptions?: (
|
||||
blockId: string,
|
||||
subBlockId: string
|
||||
) => Promise<Array<{ label: string; id: string }>>
|
||||
fetchOptions?: (blockId: string) => Promise<Array<{ label: string; id: string }>>
|
||||
/** Async function to fetch a single option's label by ID (for hydration) */
|
||||
fetchOptionById?: (
|
||||
blockId: string,
|
||||
subBlockId: string,
|
||||
optionId: string
|
||||
) => Promise<{ label: string; id: string } | null>
|
||||
/** Field dependencies that trigger option refetch when changed */
|
||||
@@ -160,7 +156,7 @@ export const Dropdown = memo(function Dropdown({
|
||||
setIsLoadingOptions(true)
|
||||
setFetchError(null)
|
||||
try {
|
||||
const options = await fetchOptions(blockId, subBlockId)
|
||||
const options = await fetchOptions(blockId)
|
||||
setFetchedOptions(options)
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : 'Failed to fetch options'
|
||||
@@ -169,7 +165,7 @@ export const Dropdown = memo(function Dropdown({
|
||||
} finally {
|
||||
setIsLoadingOptions(false)
|
||||
}
|
||||
}, [fetchOptions, blockId, subBlockId, isPreview, disabled])
|
||||
}, [fetchOptions, blockId, isPreview, disabled])
|
||||
|
||||
/**
|
||||
* Handles combobox open state changes to trigger option fetching
|
||||
@@ -430,7 +426,7 @@ export const Dropdown = memo(function Dropdown({
|
||||
let isActive = true
|
||||
|
||||
// Fetch the hydrated option
|
||||
fetchOptionById(blockId, subBlockId, valueToHydrate)
|
||||
fetchOptionById(blockId, valueToHydrate)
|
||||
.then((option) => {
|
||||
if (isActive) setHydratedOption(option)
|
||||
})
|
||||
@@ -446,7 +442,6 @@ export const Dropdown = memo(function Dropdown({
|
||||
singleValue,
|
||||
multiSelect,
|
||||
blockId,
|
||||
subBlockId,
|
||||
isPreview,
|
||||
disabled,
|
||||
fetchedOptions,
|
||||
|
||||
@@ -14,9 +14,11 @@ import {
|
||||
Unlock,
|
||||
} from 'lucide-react'
|
||||
import { useParams } from 'next/navigation'
|
||||
import { usePostHog } from 'posthog-js/react'
|
||||
import { useShallow } from 'zustand/react/shallow'
|
||||
import { useStoreWithEqualityFn } from 'zustand/traditional'
|
||||
import { Button, Tooltip } from '@/components/emcn'
|
||||
import { captureEvent } from '@/lib/posthog/client'
|
||||
import {
|
||||
buildCanonicalIndex,
|
||||
evaluateSubBlockCondition,
|
||||
@@ -106,6 +108,7 @@ export function Editor() {
|
||||
|
||||
const params = useParams()
|
||||
const workspaceId = params.workspaceId as string
|
||||
const posthog = usePostHog()
|
||||
|
||||
const subBlocksRef = useRef<HTMLDivElement>(null)
|
||||
|
||||
@@ -298,7 +301,11 @@ export function Editor() {
|
||||
const handleOpenDocs = useCallback(() => {
|
||||
const docsLink = isSubflow ? subflowConfig?.docsLink : blockConfig?.docsLink
|
||||
window.open(docsLink || 'https://docs.sim.ai/quick-reference', '_blank', 'noopener,noreferrer')
|
||||
}, [isSubflow, subflowConfig?.docsLink, blockConfig?.docsLink])
|
||||
captureEvent(posthog, 'docs_opened', {
|
||||
source: 'editor_button',
|
||||
block_type: currentBlock?.type,
|
||||
})
|
||||
}, [isSubflow, subflowConfig?.docsLink, blockConfig?.docsLink, posthog, currentBlock?.type])
|
||||
|
||||
const childWorkflowId = isWorkflowBlock ? blockSubBlockValues?.workflowId : null
|
||||
|
||||
|
||||
@@ -12,7 +12,9 @@ import {
|
||||
} from 'react'
|
||||
import clsx from 'clsx'
|
||||
import { Search } from 'lucide-react'
|
||||
import { usePostHog } from 'posthog-js/react'
|
||||
import { Button } from '@/components/emcn'
|
||||
import { captureEvent } from '@/lib/posthog/client'
|
||||
import {
|
||||
getBlocksForSidebar,
|
||||
getTriggersForSidebar,
|
||||
@@ -348,6 +350,7 @@ export const Toolbar = memo(
|
||||
triggersHeaderRef,
|
||||
})
|
||||
|
||||
const posthog = usePostHog()
|
||||
const { filterBlocks } = usePermissionConfig()
|
||||
const sandboxAllowedBlocks = useSandboxBlockConstraints()
|
||||
|
||||
@@ -541,8 +544,12 @@ export const Toolbar = memo(
|
||||
const handleViewDocumentation = useCallback(() => {
|
||||
if (activeItemInfo?.docsLink) {
|
||||
window.open(activeItemInfo.docsLink, '_blank', 'noopener,noreferrer')
|
||||
captureEvent(posthog, 'docs_opened', {
|
||||
source: 'toolbar_context_menu',
|
||||
block_type: activeItemInfo.type,
|
||||
})
|
||||
}
|
||||
}, [activeItemInfo])
|
||||
}, [activeItemInfo, posthog])
|
||||
|
||||
/**
|
||||
* Handle clicks outside the context menu to close it
|
||||
|
||||
@@ -6,6 +6,7 @@ import { Compass, MoreHorizontal } from 'lucide-react'
|
||||
import Image from 'next/image'
|
||||
import Link from 'next/link'
|
||||
import { useParams, usePathname, useRouter } from 'next/navigation'
|
||||
import { usePostHog } from 'posthog-js/react'
|
||||
import {
|
||||
Blimp,
|
||||
Button,
|
||||
@@ -39,6 +40,7 @@ import { useSession } from '@/lib/auth/auth-client'
|
||||
import { cn } from '@/lib/core/utils/cn'
|
||||
import { isMacPlatform } from '@/lib/core/utils/platform'
|
||||
import { buildFolderTree } from '@/lib/folders/tree'
|
||||
import { captureEvent } from '@/lib/posthog/client'
|
||||
import {
|
||||
START_NAV_TOUR_EVENT,
|
||||
START_WORKFLOW_TOUR_EVENT,
|
||||
@@ -315,6 +317,7 @@ export const Sidebar = memo(function Sidebar() {
|
||||
const fileInputRef = useRef<HTMLInputElement>(null)
|
||||
const scrollContainerRef = useRef<HTMLDivElement>(null)
|
||||
|
||||
const posthog = usePostHog()
|
||||
const { data: sessionData, isPending: sessionLoading } = useSession()
|
||||
const { canEdit } = useUserPermissionsContext()
|
||||
const { config: permissionConfig, filterBlocks } = usePermissionConfig()
|
||||
@@ -1092,10 +1095,10 @@ export const Sidebar = memo(function Sidebar() {
|
||||
|
||||
const handleOpenHelpFromMenu = useCallback(() => setIsHelpModalOpen(true), [])
|
||||
|
||||
const handleOpenDocs = useCallback(
|
||||
() => window.open('https://docs.sim.ai', '_blank', 'noopener,noreferrer'),
|
||||
[]
|
||||
)
|
||||
const handleOpenDocs = useCallback(() => {
|
||||
window.open('https://docs.sim.ai', '_blank', 'noopener,noreferrer')
|
||||
captureEvent(posthog, 'docs_opened', { source: 'help_menu' })
|
||||
}, [posthog])
|
||||
|
||||
const handleTaskRenameBlur = useCallback(
|
||||
() => void taskFlyoutRename.saveRename(),
|
||||
|
||||
65
apps/sim/background/lifecycle-email.ts
Normal file
65
apps/sim/background/lifecycle-email.ts
Normal file
@@ -0,0 +1,65 @@
|
||||
import { db } from '@sim/db'
|
||||
import { user } from '@sim/db/schema'
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { task } from '@trigger.dev/sdk'
|
||||
import { eq } from 'drizzle-orm'
|
||||
import { getEmailSubject, renderOnboardingFollowupEmail } from '@/components/emails'
|
||||
import { getHighestPrioritySubscription } from '@/lib/billing/core/subscription'
|
||||
import { checkEnterprisePlan } from '@/lib/billing/subscriptions/utils'
|
||||
import { sendEmail } from '@/lib/messaging/email/mailer'
|
||||
import { getPersonalEmailFrom } from '@/lib/messaging/email/utils'
|
||||
import { LIFECYCLE_EMAIL_TASK_ID, type LifecycleEmailType } from '@/lib/messaging/lifecycle'
|
||||
|
||||
const logger = createLogger('LifecycleEmail')
|
||||
|
||||
interface LifecycleEmailParams {
|
||||
userId: string
|
||||
type: LifecycleEmailType
|
||||
}
|
||||
|
||||
async function sendLifecycleEmail({ userId, type }: LifecycleEmailParams): Promise<void> {
|
||||
const [userData] = await db.select().from(user).where(eq(user.id, userId)).limit(1)
|
||||
|
||||
if (!userData?.email) {
|
||||
logger.warn('[lifecycle-email] User not found or has no email', { userId, type })
|
||||
return
|
||||
}
|
||||
|
||||
const subscription = await getHighestPrioritySubscription(userId)
|
||||
if (checkEnterprisePlan(subscription)) {
|
||||
logger.info('[lifecycle-email] Skipping lifecycle email for enterprise user', { userId, type })
|
||||
return
|
||||
}
|
||||
|
||||
const { from, replyTo } = getPersonalEmailFrom()
|
||||
|
||||
let html: string
|
||||
|
||||
switch (type) {
|
||||
case 'onboarding-followup':
|
||||
html = await renderOnboardingFollowupEmail(userData.name || undefined)
|
||||
break
|
||||
default:
|
||||
logger.warn('[lifecycle-email] Unknown lifecycle email type', { type })
|
||||
return
|
||||
}
|
||||
|
||||
await sendEmail({
|
||||
to: userData.email,
|
||||
subject: getEmailSubject(type),
|
||||
html,
|
||||
from,
|
||||
replyTo,
|
||||
emailType: 'notifications',
|
||||
})
|
||||
|
||||
logger.info('[lifecycle-email] Sent lifecycle email', { userId, type })
|
||||
}
|
||||
|
||||
export const lifecycleEmailTask = task({
|
||||
id: LIFECYCLE_EMAIL_TASK_ID,
|
||||
retry: { maxAttempts: 2 },
|
||||
run: async (params: LifecycleEmailParams) => {
|
||||
await sendLifecycleEmail(params)
|
||||
},
|
||||
})
|
||||
621
apps/sim/blocks/blocks/agentmail.ts
Normal file
621
apps/sim/blocks/blocks/agentmail.ts
Normal file
@@ -0,0 +1,621 @@
|
||||
import { AgentMailIcon } from '@/components/icons'
|
||||
import type { BlockConfig } from '@/blocks/types'
|
||||
import { AuthMode, IntegrationType } from '@/blocks/types'
|
||||
|
||||
export const AgentMailBlock: BlockConfig = {
|
||||
type: 'agentmail',
|
||||
name: 'AgentMail',
|
||||
description: 'Manage email inboxes, threads, and messages with AgentMail',
|
||||
longDescription:
|
||||
'Integrate AgentMail into your workflow. Create and manage email inboxes, send and receive messages, reply to threads, manage drafts, and organize threads with labels. Requires API Key.',
|
||||
docsLink: 'https://docs.sim.ai/tools/agentmail',
|
||||
category: 'tools',
|
||||
integrationType: IntegrationType.Email,
|
||||
tags: ['messaging'],
|
||||
bgColor: '#000000',
|
||||
icon: AgentMailIcon,
|
||||
authMode: AuthMode.ApiKey,
|
||||
|
||||
subBlocks: [
|
||||
{
|
||||
id: 'operation',
|
||||
title: 'Operation',
|
||||
type: 'dropdown',
|
||||
options: [
|
||||
{ label: 'Send Message', id: 'send_message' },
|
||||
{ label: 'Reply to Message', id: 'reply_message' },
|
||||
{ label: 'Forward Message', id: 'forward_message' },
|
||||
{ label: 'List Threads', id: 'list_threads' },
|
||||
{ label: 'Get Thread', id: 'get_thread' },
|
||||
{ label: 'Update Thread Labels', id: 'update_thread' },
|
||||
{ label: 'Delete Thread', id: 'delete_thread' },
|
||||
{ label: 'List Messages', id: 'list_messages' },
|
||||
{ label: 'Get Message', id: 'get_message' },
|
||||
{ label: 'Update Message Labels', id: 'update_message' },
|
||||
{ label: 'Create Draft', id: 'create_draft' },
|
||||
{ label: 'List Drafts', id: 'list_drafts' },
|
||||
{ label: 'Get Draft', id: 'get_draft' },
|
||||
{ label: 'Update Draft', id: 'update_draft' },
|
||||
{ label: 'Delete Draft', id: 'delete_draft' },
|
||||
{ label: 'Send Draft', id: 'send_draft' },
|
||||
{ label: 'Create Inbox', id: 'create_inbox' },
|
||||
{ label: 'List Inboxes', id: 'list_inboxes' },
|
||||
{ label: 'Get Inbox', id: 'get_inbox' },
|
||||
{ label: 'Update Inbox', id: 'update_inbox' },
|
||||
{ label: 'Delete Inbox', id: 'delete_inbox' },
|
||||
],
|
||||
value: () => 'send_message',
|
||||
},
|
||||
{
|
||||
id: 'apiKey',
|
||||
title: 'API Key',
|
||||
type: 'short-input',
|
||||
placeholder: 'Enter your AgentMail API key',
|
||||
required: true,
|
||||
password: true,
|
||||
},
|
||||
|
||||
// Send Message fields
|
||||
{
|
||||
id: 'inboxId',
|
||||
title: 'Inbox ID',
|
||||
type: 'short-input',
|
||||
placeholder: 'Inbox ID',
|
||||
condition: {
|
||||
field: 'operation',
|
||||
value: [
|
||||
'send_message',
|
||||
'reply_message',
|
||||
'forward_message',
|
||||
'list_threads',
|
||||
'get_thread',
|
||||
'update_thread',
|
||||
'delete_thread',
|
||||
'list_messages',
|
||||
'get_message',
|
||||
'update_message',
|
||||
'create_draft',
|
||||
'list_drafts',
|
||||
'get_draft',
|
||||
'update_draft',
|
||||
'delete_draft',
|
||||
'send_draft',
|
||||
],
|
||||
},
|
||||
required: {
|
||||
field: 'operation',
|
||||
value: [
|
||||
'send_message',
|
||||
'reply_message',
|
||||
'forward_message',
|
||||
'list_threads',
|
||||
'get_thread',
|
||||
'update_thread',
|
||||
'delete_thread',
|
||||
'list_messages',
|
||||
'get_message',
|
||||
'update_message',
|
||||
'create_draft',
|
||||
'list_drafts',
|
||||
'get_draft',
|
||||
'update_draft',
|
||||
'delete_draft',
|
||||
'send_draft',
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'to',
|
||||
title: 'To',
|
||||
type: 'short-input',
|
||||
placeholder: 'recipient@example.com',
|
||||
condition: {
|
||||
field: 'operation',
|
||||
value: ['send_message', 'forward_message', 'create_draft', 'update_draft'],
|
||||
},
|
||||
required: { field: 'operation', value: ['send_message', 'forward_message'] },
|
||||
},
|
||||
{
|
||||
id: 'subject',
|
||||
title: 'Subject',
|
||||
type: 'short-input',
|
||||
placeholder: 'Email subject',
|
||||
condition: {
|
||||
field: 'operation',
|
||||
value: ['send_message', 'forward_message', 'create_draft', 'update_draft'],
|
||||
},
|
||||
required: { field: 'operation', value: 'send_message' },
|
||||
wandConfig: {
|
||||
enabled: true,
|
||||
prompt:
|
||||
'Generate a compelling email subject line based on the description. Keep it concise. Return ONLY the subject line.',
|
||||
placeholder: 'Describe the email topic...',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'text',
|
||||
title: 'Text',
|
||||
type: 'long-input',
|
||||
placeholder: 'Plain text email body',
|
||||
condition: {
|
||||
field: 'operation',
|
||||
value: ['send_message', 'reply_message', 'forward_message', 'create_draft', 'update_draft'],
|
||||
},
|
||||
wandConfig: {
|
||||
enabled: true,
|
||||
prompt:
|
||||
'Generate email content based on the description. Use clear formatting with short paragraphs. Return ONLY the email body.',
|
||||
placeholder: 'Describe the email content...',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'html',
|
||||
title: 'HTML',
|
||||
type: 'long-input',
|
||||
placeholder: '<p>HTML email body</p>',
|
||||
condition: {
|
||||
field: 'operation',
|
||||
value: ['send_message', 'reply_message', 'forward_message', 'create_draft', 'update_draft'],
|
||||
},
|
||||
mode: 'advanced',
|
||||
},
|
||||
{
|
||||
id: 'cc',
|
||||
title: 'CC',
|
||||
type: 'short-input',
|
||||
placeholder: 'cc@example.com',
|
||||
condition: {
|
||||
field: 'operation',
|
||||
value: ['send_message', 'reply_message', 'forward_message', 'create_draft', 'update_draft'],
|
||||
},
|
||||
mode: 'advanced',
|
||||
},
|
||||
{
|
||||
id: 'bcc',
|
||||
title: 'BCC',
|
||||
type: 'short-input',
|
||||
placeholder: 'bcc@example.com',
|
||||
condition: {
|
||||
field: 'operation',
|
||||
value: ['send_message', 'reply_message', 'forward_message', 'create_draft', 'update_draft'],
|
||||
},
|
||||
mode: 'advanced',
|
||||
},
|
||||
|
||||
// Reply to Message fields
|
||||
{
|
||||
id: 'replyMessageId',
|
||||
title: 'Message ID to Reply To',
|
||||
type: 'short-input',
|
||||
placeholder: 'Message ID',
|
||||
condition: { field: 'operation', value: 'reply_message' },
|
||||
required: { field: 'operation', value: 'reply_message' },
|
||||
},
|
||||
{
|
||||
id: 'replyTo',
|
||||
title: 'Override To',
|
||||
type: 'short-input',
|
||||
placeholder: 'Override recipient (optional)',
|
||||
condition: { field: 'operation', value: 'reply_message' },
|
||||
mode: 'advanced',
|
||||
},
|
||||
{
|
||||
id: 'replyAll',
|
||||
title: 'Reply All',
|
||||
type: 'dropdown',
|
||||
options: [
|
||||
{ label: 'No', id: 'false' },
|
||||
{ label: 'Yes', id: 'true' },
|
||||
],
|
||||
value: () => 'false',
|
||||
condition: { field: 'operation', value: 'reply_message' },
|
||||
mode: 'advanced',
|
||||
},
|
||||
|
||||
// Thread ID fields (shared across thread operations)
|
||||
{
|
||||
id: 'threadId',
|
||||
title: 'Thread ID',
|
||||
type: 'short-input',
|
||||
placeholder: 'Thread ID',
|
||||
condition: {
|
||||
field: 'operation',
|
||||
value: ['get_thread', 'update_thread', 'delete_thread'],
|
||||
},
|
||||
required: {
|
||||
field: 'operation',
|
||||
value: ['get_thread', 'update_thread', 'delete_thread'],
|
||||
},
|
||||
},
|
||||
|
||||
// Update Thread Labels fields
|
||||
{
|
||||
id: 'addLabels',
|
||||
title: 'Add Labels',
|
||||
type: 'short-input',
|
||||
placeholder: 'important, follow-up',
|
||||
condition: { field: 'operation', value: 'update_thread' },
|
||||
},
|
||||
{
|
||||
id: 'removeLabels',
|
||||
title: 'Remove Labels',
|
||||
type: 'short-input',
|
||||
placeholder: 'inbox, unread',
|
||||
condition: { field: 'operation', value: 'update_thread' },
|
||||
},
|
||||
|
||||
// Delete Thread fields
|
||||
{
|
||||
id: 'permanent',
|
||||
title: 'Permanent Delete',
|
||||
type: 'dropdown',
|
||||
options: [
|
||||
{ label: 'No (move to trash)', id: 'false' },
|
||||
{ label: 'Yes (permanent)', id: 'true' },
|
||||
],
|
||||
value: () => 'false',
|
||||
condition: { field: 'operation', value: 'delete_thread' },
|
||||
mode: 'advanced',
|
||||
},
|
||||
|
||||
// Forward Message fields
|
||||
{
|
||||
id: 'forwardMessageId',
|
||||
title: 'Message ID to Forward',
|
||||
type: 'short-input',
|
||||
placeholder: 'Message ID',
|
||||
condition: { field: 'operation', value: 'forward_message' },
|
||||
required: { field: 'operation', value: 'forward_message' },
|
||||
},
|
||||
|
||||
// Update Message Labels fields
|
||||
{
|
||||
id: 'updateMessageId',
|
||||
title: 'Message ID',
|
||||
type: 'short-input',
|
||||
placeholder: 'Message ID',
|
||||
condition: { field: 'operation', value: 'update_message' },
|
||||
required: { field: 'operation', value: 'update_message' },
|
||||
},
|
||||
{
|
||||
id: 'msgAddLabels',
|
||||
title: 'Add Labels',
|
||||
type: 'short-input',
|
||||
placeholder: 'important, follow-up',
|
||||
condition: { field: 'operation', value: 'update_message' },
|
||||
},
|
||||
{
|
||||
id: 'msgRemoveLabels',
|
||||
title: 'Remove Labels',
|
||||
type: 'short-input',
|
||||
placeholder: 'inbox, unread',
|
||||
condition: { field: 'operation', value: 'update_message' },
|
||||
},
|
||||
|
||||
// Get Message fields
|
||||
{
|
||||
id: 'messageId',
|
||||
title: 'Message ID',
|
||||
type: 'short-input',
|
||||
placeholder: 'Message ID',
|
||||
condition: { field: 'operation', value: 'get_message' },
|
||||
required: { field: 'operation', value: 'get_message' },
|
||||
},
|
||||
|
||||
// Draft ID fields (shared across draft operations)
|
||||
{
|
||||
id: 'draftId',
|
||||
title: 'Draft ID',
|
||||
type: 'short-input',
|
||||
placeholder: 'Draft ID',
|
||||
condition: {
|
||||
field: 'operation',
|
||||
value: ['get_draft', 'update_draft', 'delete_draft', 'send_draft'],
|
||||
},
|
||||
required: {
|
||||
field: 'operation',
|
||||
value: ['get_draft', 'update_draft', 'delete_draft', 'send_draft'],
|
||||
},
|
||||
},
|
||||
|
||||
// Create/Update Draft fields
|
||||
{
|
||||
id: 'draftInReplyTo',
|
||||
title: 'In Reply To',
|
||||
type: 'short-input',
|
||||
placeholder: 'Message ID this draft replies to',
|
||||
condition: { field: 'operation', value: 'create_draft' },
|
||||
mode: 'advanced',
|
||||
},
|
||||
{
|
||||
id: 'sendAt',
|
||||
title: 'Schedule Send',
|
||||
type: 'short-input',
|
||||
placeholder: 'ISO 8601 timestamp to schedule sending',
|
||||
condition: { field: 'operation', value: ['create_draft', 'update_draft'] },
|
||||
mode: 'advanced',
|
||||
wandConfig: {
|
||||
enabled: true,
|
||||
generationType: 'timestamp',
|
||||
prompt: 'Generate an ISO 8601 timestamp. Return ONLY the timestamp string.',
|
||||
placeholder: 'Describe when to send (e.g., "tomorrow at 9am")...',
|
||||
},
|
||||
},
|
||||
|
||||
// Create Inbox fields
|
||||
{
|
||||
id: 'username',
|
||||
title: 'Username',
|
||||
type: 'short-input',
|
||||
placeholder: 'Optional username for email address',
|
||||
condition: { field: 'operation', value: 'create_inbox' },
|
||||
},
|
||||
{
|
||||
id: 'domain',
|
||||
title: 'Domain',
|
||||
type: 'short-input',
|
||||
placeholder: 'Optional domain for email address',
|
||||
condition: { field: 'operation', value: 'create_inbox' },
|
||||
mode: 'advanced',
|
||||
},
|
||||
{
|
||||
id: 'displayName',
|
||||
title: 'Display Name',
|
||||
type: 'short-input',
|
||||
placeholder: 'Inbox display name',
|
||||
condition: { field: 'operation', value: ['create_inbox', 'update_inbox'] },
|
||||
required: { field: 'operation', value: 'update_inbox' },
|
||||
},
|
||||
|
||||
// Inbox ID for get/update/delete inbox
|
||||
{
|
||||
id: 'inboxIdParam',
|
||||
title: 'Inbox ID',
|
||||
type: 'short-input',
|
||||
placeholder: 'Inbox ID',
|
||||
condition: {
|
||||
field: 'operation',
|
||||
value: ['get_inbox', 'update_inbox', 'delete_inbox'],
|
||||
},
|
||||
required: {
|
||||
field: 'operation',
|
||||
value: ['get_inbox', 'update_inbox', 'delete_inbox'],
|
||||
},
|
||||
},
|
||||
|
||||
// Pagination fields (advanced)
|
||||
{
|
||||
id: 'limit',
|
||||
title: 'Limit',
|
||||
type: 'short-input',
|
||||
placeholder: 'Max results to return',
|
||||
condition: {
|
||||
field: 'operation',
|
||||
value: ['list_inboxes', 'list_threads', 'list_messages', 'list_drafts'],
|
||||
},
|
||||
mode: 'advanced',
|
||||
},
|
||||
{
|
||||
id: 'pageToken',
|
||||
title: 'Page Token',
|
||||
type: 'short-input',
|
||||
placeholder: 'Pagination token',
|
||||
condition: {
|
||||
field: 'operation',
|
||||
value: ['list_inboxes', 'list_threads', 'list_messages', 'list_drafts'],
|
||||
},
|
||||
mode: 'advanced',
|
||||
},
|
||||
|
||||
// List Threads filters (advanced)
|
||||
{
|
||||
id: 'labels',
|
||||
title: 'Labels Filter',
|
||||
type: 'short-input',
|
||||
placeholder: 'Filter by labels (comma-separated)',
|
||||
condition: { field: 'operation', value: 'list_threads' },
|
||||
mode: 'advanced',
|
||||
},
|
||||
{
|
||||
id: 'before',
|
||||
title: 'Before',
|
||||
type: 'short-input',
|
||||
placeholder: 'Filter threads before this date',
|
||||
condition: { field: 'operation', value: 'list_threads' },
|
||||
mode: 'advanced',
|
||||
wandConfig: {
|
||||
enabled: true,
|
||||
generationType: 'timestamp',
|
||||
prompt: 'Generate an ISO 8601 timestamp. Return ONLY the timestamp string.',
|
||||
placeholder: 'Describe the date (e.g., "yesterday")...',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'after',
|
||||
title: 'After',
|
||||
type: 'short-input',
|
||||
placeholder: 'Filter threads after this date',
|
||||
condition: { field: 'operation', value: 'list_threads' },
|
||||
mode: 'advanced',
|
||||
wandConfig: {
|
||||
enabled: true,
|
||||
generationType: 'timestamp',
|
||||
prompt: 'Generate an ISO 8601 timestamp. Return ONLY the timestamp string.',
|
||||
placeholder: 'Describe the date (e.g., "last week")...',
|
||||
},
|
||||
},
|
||||
],
|
||||
|
||||
tools: {
|
||||
access: [
|
||||
'agentmail_create_draft',
|
||||
'agentmail_create_inbox',
|
||||
'agentmail_delete_draft',
|
||||
'agentmail_delete_inbox',
|
||||
'agentmail_delete_thread',
|
||||
'agentmail_forward_message',
|
||||
'agentmail_get_draft',
|
||||
'agentmail_get_inbox',
|
||||
'agentmail_get_message',
|
||||
'agentmail_get_thread',
|
||||
'agentmail_list_drafts',
|
||||
'agentmail_list_inboxes',
|
||||
'agentmail_list_messages',
|
||||
'agentmail_list_threads',
|
||||
'agentmail_reply_message',
|
||||
'agentmail_send_draft',
|
||||
'agentmail_send_message',
|
||||
'agentmail_update_draft',
|
||||
'agentmail_update_inbox',
|
||||
'agentmail_update_message',
|
||||
'agentmail_update_thread',
|
||||
],
|
||||
config: {
|
||||
tool: (params) => `agentmail_${params.operation || 'send_message'}`,
|
||||
params: (params) => {
|
||||
const {
|
||||
operation,
|
||||
inboxIdParam,
|
||||
permanent,
|
||||
replyMessageId,
|
||||
replyTo,
|
||||
replyAll,
|
||||
forwardMessageId,
|
||||
updateMessageId,
|
||||
msgAddLabels,
|
||||
msgRemoveLabels,
|
||||
addLabels,
|
||||
removeLabels,
|
||||
draftInReplyTo,
|
||||
...rest
|
||||
} = params
|
||||
|
||||
if (['get_inbox', 'update_inbox', 'delete_inbox'].includes(operation) && inboxIdParam) {
|
||||
rest.inboxId = inboxIdParam
|
||||
}
|
||||
|
||||
if (operation === 'delete_thread' && permanent !== undefined) {
|
||||
rest.permanent = permanent === 'true'
|
||||
}
|
||||
|
||||
if (operation === 'reply_message' && replyAll !== undefined) {
|
||||
rest.replyAll = replyAll === 'true'
|
||||
}
|
||||
|
||||
if (operation === 'reply_message' && replyMessageId) {
|
||||
rest.messageId = replyMessageId
|
||||
}
|
||||
|
||||
if (operation === 'reply_message' && replyTo) {
|
||||
rest.to = replyTo
|
||||
} else if (operation === 'reply_message') {
|
||||
rest.to = undefined
|
||||
}
|
||||
|
||||
if (operation === 'forward_message' && forwardMessageId) {
|
||||
rest.messageId = forwardMessageId
|
||||
}
|
||||
|
||||
if (operation === 'update_message' && updateMessageId) {
|
||||
rest.messageId = updateMessageId
|
||||
}
|
||||
|
||||
if (operation === 'update_message' && msgAddLabels) {
|
||||
rest.addLabels = msgAddLabels
|
||||
}
|
||||
|
||||
if (operation === 'update_message' && msgRemoveLabels) {
|
||||
rest.removeLabels = msgRemoveLabels
|
||||
}
|
||||
|
||||
if (operation === 'update_thread' && addLabels) {
|
||||
rest.addLabels = addLabels
|
||||
}
|
||||
|
||||
if (operation === 'update_thread' && removeLabels) {
|
||||
rest.removeLabels = removeLabels
|
||||
}
|
||||
|
||||
if (operation === 'create_draft' && draftInReplyTo) {
|
||||
rest.inReplyTo = draftInReplyTo
|
||||
}
|
||||
|
||||
if (rest.limit) {
|
||||
rest.limit = Number(rest.limit)
|
||||
}
|
||||
|
||||
return rest
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
inputs: {
|
||||
operation: { type: 'string', description: 'Operation to perform' },
|
||||
apiKey: { type: 'string', description: 'AgentMail API key' },
|
||||
inboxId: { type: 'string', description: 'Inbox ID' },
|
||||
inboxIdParam: {
|
||||
type: 'string',
|
||||
description: 'Inbox ID for get/update/delete inbox operations',
|
||||
},
|
||||
to: { type: 'string', description: 'Recipient email address' },
|
||||
subject: { type: 'string', description: 'Email subject' },
|
||||
text: { type: 'string', description: 'Plain text email body' },
|
||||
html: { type: 'string', description: 'HTML email body' },
|
||||
cc: { type: 'string', description: 'CC email addresses' },
|
||||
bcc: { type: 'string', description: 'BCC email addresses' },
|
||||
replyMessageId: { type: 'string', description: 'Message ID to reply to' },
|
||||
replyTo: { type: 'string', description: 'Override recipient for reply' },
|
||||
replyAll: { type: 'string', description: 'Reply to all recipients' },
|
||||
forwardMessageId: { type: 'string', description: 'Message ID to forward' },
|
||||
updateMessageId: { type: 'string', description: 'Message ID to update labels on' },
|
||||
msgAddLabels: { type: 'string', description: 'Labels to add to message' },
|
||||
msgRemoveLabels: { type: 'string', description: 'Labels to remove from message' },
|
||||
threadId: { type: 'string', description: 'Thread ID' },
|
||||
addLabels: { type: 'string', description: 'Labels to add to thread (comma-separated)' },
|
||||
removeLabels: { type: 'string', description: 'Labels to remove from thread (comma-separated)' },
|
||||
permanent: { type: 'string', description: 'Whether to permanently delete' },
|
||||
messageId: { type: 'string', description: 'Message ID' },
|
||||
draftId: { type: 'string', description: 'Draft ID' },
|
||||
draftInReplyTo: { type: 'string', description: 'Message ID this draft replies to' },
|
||||
sendAt: { type: 'string', description: 'ISO 8601 timestamp to schedule sending' },
|
||||
username: { type: 'string', description: 'Username for new inbox' },
|
||||
domain: { type: 'string', description: 'Domain for new inbox' },
|
||||
displayName: { type: 'string', description: 'Display name for inbox' },
|
||||
limit: { type: 'string', description: 'Max results to return' },
|
||||
pageToken: { type: 'string', description: 'Pagination token' },
|
||||
labels: { type: 'string', description: 'Labels filter for threads' },
|
||||
before: { type: 'string', description: 'Filter threads before this date' },
|
||||
after: { type: 'string', description: 'Filter threads after this date' },
|
||||
},
|
||||
|
||||
outputs: {
|
||||
inboxId: { type: 'string', description: 'Inbox ID' },
|
||||
email: { type: 'string', description: 'Inbox email address' },
|
||||
displayName: { type: 'string', description: 'Inbox display name' },
|
||||
threadId: { type: 'string', description: 'Thread ID' },
|
||||
messageId: { type: 'string', description: 'Message ID' },
|
||||
draftId: { type: 'string', description: 'Draft ID' },
|
||||
subject: { type: 'string', description: 'Email subject' },
|
||||
to: { type: 'string', description: 'Recipient email address' },
|
||||
from: { type: 'string', description: 'Sender email address' },
|
||||
text: { type: 'string', description: 'Plain text content' },
|
||||
html: { type: 'string', description: 'HTML content' },
|
||||
preview: { type: 'string', description: 'Message or draft preview text' },
|
||||
senders: { type: 'json', description: 'List of sender email addresses' },
|
||||
recipients: { type: 'json', description: 'List of recipient email addresses' },
|
||||
labels: { type: 'json', description: 'Thread or draft labels' },
|
||||
messages: { type: 'json', description: 'List of messages' },
|
||||
threads: { type: 'json', description: 'List of threads' },
|
||||
inboxes: { type: 'json', description: 'List of inboxes' },
|
||||
drafts: { type: 'json', description: 'List of drafts' },
|
||||
messageCount: { type: 'number', description: 'Number of messages in thread' },
|
||||
count: { type: 'number', description: 'Total number of results' },
|
||||
nextPageToken: { type: 'string', description: 'Token for next page of results' },
|
||||
deleted: { type: 'boolean', description: 'Whether the resource was deleted' },
|
||||
sendStatus: { type: 'string', description: 'Draft send status' },
|
||||
sendAt: { type: 'string', description: 'Scheduled send time' },
|
||||
inReplyTo: { type: 'string', description: 'Message ID this draft replies to' },
|
||||
createdAt: { type: 'string', description: 'Creation timestamp' },
|
||||
updatedAt: { type: 'string', description: 'Last updated timestamp' },
|
||||
},
|
||||
}
|
||||
151
apps/sim/blocks/blocks/credential.ts
Normal file
151
apps/sim/blocks/blocks/credential.ts
Normal file
@@ -0,0 +1,151 @@
|
||||
import { CredentialIcon } from '@/components/icons'
|
||||
import { getServiceConfigByProviderId } from '@/lib/oauth/utils'
|
||||
import { getQueryClient } from '@/app/_shell/providers/get-query-client'
|
||||
import type { BlockConfig } from '@/blocks/types'
|
||||
import { fetchWorkspaceCredentialList, workspaceCredentialKeys } from '@/hooks/queries/credentials'
|
||||
import { useWorkflowRegistry } from '@/stores/workflows/registry/store'
|
||||
|
||||
interface CredentialBlockOutput {
|
||||
success: boolean
|
||||
output: {
|
||||
credentialId: string
|
||||
displayName: string
|
||||
providerId: string
|
||||
credentials: Array<{
|
||||
credentialId: string
|
||||
displayName: string
|
||||
providerId: string
|
||||
}>
|
||||
count: number
|
||||
}
|
||||
}
|
||||
|
||||
export const CredentialBlock: BlockConfig<CredentialBlockOutput> = {
|
||||
type: 'credential',
|
||||
name: 'Credential',
|
||||
description: 'Select or list OAuth credentials',
|
||||
longDescription:
|
||||
'Select an OAuth credential once and pipe its ID into any downstream block that requires authentication, or list all OAuth credentials in the workspace for iteration. No secrets are ever exposed — only credential IDs and metadata.',
|
||||
bestPractices: `
|
||||
- Use "Select Credential" to define an OAuth credential once and reference <CredentialBlock.credentialId> in multiple downstream blocks instead of repeating credential IDs.
|
||||
- Use "List Credentials" with a ForEach loop to iterate over all OAuth accounts (e.g. all Gmail accounts).
|
||||
- Use the Provider filter to narrow results to specific services (e.g. Gmail, Slack).
|
||||
- The outputs are credential ID references, not secret values — they are safe to log and inspect.
|
||||
- To switch credentials across environments, replace the single Credential block rather than updating every downstream block.
|
||||
`,
|
||||
docsLink: 'https://docs.sim.ai/blocks/credential',
|
||||
bgColor: '#6366F1',
|
||||
icon: CredentialIcon,
|
||||
category: 'blocks',
|
||||
subBlocks: [
|
||||
{
|
||||
id: 'operation',
|
||||
title: 'Operation',
|
||||
type: 'dropdown',
|
||||
options: [
|
||||
{ label: 'Select Credential', id: 'select' },
|
||||
{ label: 'List Credentials', id: 'list' },
|
||||
],
|
||||
value: () => 'select',
|
||||
},
|
||||
{
|
||||
id: 'providerFilter',
|
||||
title: 'Provider',
|
||||
type: 'dropdown',
|
||||
multiSelect: true,
|
||||
options: [],
|
||||
condition: { field: 'operation', value: 'list' },
|
||||
fetchOptions: async () => {
|
||||
const workspaceId = useWorkflowRegistry.getState().hydration.workspaceId
|
||||
if (!workspaceId) return []
|
||||
|
||||
const credentials = await getQueryClient().fetchQuery({
|
||||
queryKey: workspaceCredentialKeys.list(workspaceId),
|
||||
queryFn: () => fetchWorkspaceCredentialList(workspaceId),
|
||||
staleTime: 60 * 1000,
|
||||
})
|
||||
|
||||
const seen = new Set<string>()
|
||||
const options: Array<{ label: string; id: string }> = []
|
||||
|
||||
for (const cred of credentials) {
|
||||
if (cred.type === 'oauth' && cred.providerId && !seen.has(cred.providerId)) {
|
||||
seen.add(cred.providerId)
|
||||
const serviceConfig = getServiceConfigByProviderId(cred.providerId)
|
||||
options.push({ label: serviceConfig?.name ?? cred.providerId, id: cred.providerId })
|
||||
}
|
||||
}
|
||||
|
||||
return options.sort((a, b) => a.label.localeCompare(b.label))
|
||||
},
|
||||
fetchOptionById: async (_blockId: string, optionId: string) => {
|
||||
const serviceConfig = getServiceConfigByProviderId(optionId)
|
||||
const label = serviceConfig?.name ?? optionId
|
||||
return { label, id: optionId }
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'credential',
|
||||
title: 'Credential',
|
||||
type: 'oauth-input',
|
||||
required: { field: 'operation', value: 'select' },
|
||||
mode: 'basic',
|
||||
placeholder: 'Select a credential',
|
||||
canonicalParamId: 'credentialId',
|
||||
condition: { field: 'operation', value: 'select' },
|
||||
},
|
||||
{
|
||||
id: 'manualCredential',
|
||||
title: 'Credential ID',
|
||||
type: 'short-input',
|
||||
required: { field: 'operation', value: 'select' },
|
||||
mode: 'advanced',
|
||||
placeholder: 'Enter credential ID',
|
||||
canonicalParamId: 'credentialId',
|
||||
condition: { field: 'operation', value: 'select' },
|
||||
},
|
||||
],
|
||||
tools: {
|
||||
access: [],
|
||||
},
|
||||
inputs: {
|
||||
operation: { type: 'string', description: "'select' or 'list'" },
|
||||
credentialId: {
|
||||
type: 'string',
|
||||
description: 'The OAuth credential ID to resolve (select operation)',
|
||||
},
|
||||
providerFilter: {
|
||||
type: 'json',
|
||||
description:
|
||||
'Array of OAuth provider IDs to filter by (e.g. ["google-email", "slack"]). Leave empty to return all OAuth credentials.',
|
||||
},
|
||||
},
|
||||
outputs: {
|
||||
credentialId: {
|
||||
type: 'string',
|
||||
description: "Credential ID — pipe into other blocks' credential fields",
|
||||
condition: { field: 'operation', value: 'select' },
|
||||
},
|
||||
displayName: {
|
||||
type: 'string',
|
||||
description: 'Human-readable name of the credential',
|
||||
condition: { field: 'operation', value: 'select' },
|
||||
},
|
||||
providerId: {
|
||||
type: 'string',
|
||||
description: 'OAuth provider ID (e.g. google-email, slack)',
|
||||
condition: { field: 'operation', value: 'select' },
|
||||
},
|
||||
credentials: {
|
||||
type: 'json',
|
||||
description:
|
||||
'Array of OAuth credential objects, each with credentialId, displayName, and providerId',
|
||||
condition: { field: 'operation', value: 'list' },
|
||||
},
|
||||
count: {
|
||||
type: 'number',
|
||||
description: 'Number of credentials returned',
|
||||
condition: { field: 'operation', value: 'list' },
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -36,11 +36,23 @@ export const RootlyBlock: BlockConfig<RootlyResponse> = {
|
||||
{ label: 'List Incident Types', id: 'rootly_list_incident_types' },
|
||||
{ label: 'List Functionalities', id: 'rootly_list_functionalities' },
|
||||
{ label: 'List Retrospectives', id: 'rootly_list_retrospectives' },
|
||||
{ label: 'Delete Incident', id: 'rootly_delete_incident' },
|
||||
{ label: 'Get Alert', id: 'rootly_get_alert' },
|
||||
{ label: 'Update Alert', id: 'rootly_update_alert' },
|
||||
{ label: 'Acknowledge Alert', id: 'rootly_acknowledge_alert' },
|
||||
{ label: 'Resolve Alert', id: 'rootly_resolve_alert' },
|
||||
{ label: 'Create Action Item', id: 'rootly_create_action_item' },
|
||||
{ label: 'List Action Items', id: 'rootly_list_action_items' },
|
||||
{ label: 'List Users', id: 'rootly_list_users' },
|
||||
{ label: 'List On-Calls', id: 'rootly_list_on_calls' },
|
||||
{ label: 'List Schedules', id: 'rootly_list_schedules' },
|
||||
{ label: 'List Escalation Policies', id: 'rootly_list_escalation_policies' },
|
||||
{ label: 'List Causes', id: 'rootly_list_causes' },
|
||||
{ label: 'List Playbooks', id: 'rootly_list_playbooks' },
|
||||
],
|
||||
value: () => 'rootly_create_incident',
|
||||
},
|
||||
|
||||
// Create Incident fields
|
||||
{
|
||||
id: 'title',
|
||||
title: 'Title',
|
||||
@@ -170,7 +182,6 @@ export const RootlyBlock: BlockConfig<RootlyResponse> = {
|
||||
mode: 'advanced',
|
||||
},
|
||||
|
||||
// Get Incident fields
|
||||
{
|
||||
id: 'getIncidentId',
|
||||
title: 'Incident ID',
|
||||
@@ -180,7 +191,6 @@ export const RootlyBlock: BlockConfig<RootlyResponse> = {
|
||||
required: { field: 'operation', value: 'rootly_get_incident' },
|
||||
},
|
||||
|
||||
// Update Incident fields
|
||||
{
|
||||
id: 'updateIncidentId',
|
||||
title: 'Incident ID',
|
||||
@@ -341,7 +351,6 @@ export const RootlyBlock: BlockConfig<RootlyResponse> = {
|
||||
mode: 'advanced',
|
||||
},
|
||||
|
||||
// List Incidents fields
|
||||
{
|
||||
id: 'listIncidentsStatus',
|
||||
title: 'Status Filter',
|
||||
@@ -434,7 +443,6 @@ export const RootlyBlock: BlockConfig<RootlyResponse> = {
|
||||
mode: 'advanced',
|
||||
},
|
||||
|
||||
// Create Alert fields
|
||||
{
|
||||
id: 'alertSummary',
|
||||
title: 'Summary',
|
||||
@@ -520,7 +528,6 @@ export const RootlyBlock: BlockConfig<RootlyResponse> = {
|
||||
mode: 'advanced',
|
||||
},
|
||||
|
||||
// List Alerts fields
|
||||
{
|
||||
id: 'listAlertsStatus',
|
||||
title: 'Status Filter',
|
||||
@@ -585,7 +592,6 @@ export const RootlyBlock: BlockConfig<RootlyResponse> = {
|
||||
mode: 'advanced',
|
||||
},
|
||||
|
||||
// Add Incident Event fields
|
||||
{
|
||||
id: 'eventIncidentId',
|
||||
title: 'Incident ID',
|
||||
@@ -616,7 +622,6 @@ export const RootlyBlock: BlockConfig<RootlyResponse> = {
|
||||
mode: 'advanced',
|
||||
},
|
||||
|
||||
// List Services fields
|
||||
{
|
||||
id: 'servicesSearch',
|
||||
title: 'Search',
|
||||
@@ -641,7 +646,6 @@ export const RootlyBlock: BlockConfig<RootlyResponse> = {
|
||||
mode: 'advanced',
|
||||
},
|
||||
|
||||
// List Severities fields
|
||||
{
|
||||
id: 'severitiesSearch',
|
||||
title: 'Search',
|
||||
@@ -666,7 +670,6 @@ export const RootlyBlock: BlockConfig<RootlyResponse> = {
|
||||
mode: 'advanced',
|
||||
},
|
||||
|
||||
// List Teams fields
|
||||
{
|
||||
id: 'teamsSearch',
|
||||
title: 'Search',
|
||||
@@ -691,7 +694,6 @@ export const RootlyBlock: BlockConfig<RootlyResponse> = {
|
||||
mode: 'advanced',
|
||||
},
|
||||
|
||||
// List Environments fields
|
||||
{
|
||||
id: 'environmentsSearch',
|
||||
title: 'Search',
|
||||
@@ -716,7 +718,6 @@ export const RootlyBlock: BlockConfig<RootlyResponse> = {
|
||||
mode: 'advanced',
|
||||
},
|
||||
|
||||
// List Incident Types fields
|
||||
{
|
||||
id: 'incidentTypesSearch',
|
||||
title: 'Name Filter',
|
||||
@@ -741,7 +742,6 @@ export const RootlyBlock: BlockConfig<RootlyResponse> = {
|
||||
mode: 'advanced',
|
||||
},
|
||||
|
||||
// List Functionalities fields
|
||||
{
|
||||
id: 'functionalitiesSearch',
|
||||
title: 'Search',
|
||||
@@ -766,7 +766,6 @@ export const RootlyBlock: BlockConfig<RootlyResponse> = {
|
||||
mode: 'advanced',
|
||||
},
|
||||
|
||||
// List Retrospectives fields
|
||||
{
|
||||
id: 'retrospectivesStatus',
|
||||
title: 'Status Filter',
|
||||
@@ -803,7 +802,404 @@ export const RootlyBlock: BlockConfig<RootlyResponse> = {
|
||||
mode: 'advanced',
|
||||
},
|
||||
|
||||
// API Key (common)
|
||||
{
|
||||
id: 'deleteIncidentId',
|
||||
title: 'Incident ID',
|
||||
type: 'short-input',
|
||||
placeholder: 'The ID of the incident to delete',
|
||||
condition: { field: 'operation', value: 'rootly_delete_incident' },
|
||||
required: { field: 'operation', value: 'rootly_delete_incident' },
|
||||
},
|
||||
|
||||
{
|
||||
id: 'getAlertId',
|
||||
title: 'Alert ID',
|
||||
type: 'short-input',
|
||||
placeholder: 'The ID of the alert to retrieve',
|
||||
condition: { field: 'operation', value: 'rootly_get_alert' },
|
||||
required: { field: 'operation', value: 'rootly_get_alert' },
|
||||
},
|
||||
|
||||
{
|
||||
id: 'updateAlertId',
|
||||
title: 'Alert ID',
|
||||
type: 'short-input',
|
||||
placeholder: 'The ID of the alert to update',
|
||||
condition: { field: 'operation', value: 'rootly_update_alert' },
|
||||
required: { field: 'operation', value: 'rootly_update_alert' },
|
||||
},
|
||||
{
|
||||
id: 'updateAlertSummary',
|
||||
title: 'Summary',
|
||||
type: 'short-input',
|
||||
placeholder: 'Updated alert summary',
|
||||
condition: { field: 'operation', value: 'rootly_update_alert' },
|
||||
},
|
||||
{
|
||||
id: 'updateAlertDescription',
|
||||
title: 'Description',
|
||||
type: 'long-input',
|
||||
placeholder: 'Updated alert description',
|
||||
condition: { field: 'operation', value: 'rootly_update_alert' },
|
||||
},
|
||||
{
|
||||
id: 'updateAlertSource',
|
||||
title: 'Source',
|
||||
type: 'short-input',
|
||||
placeholder: 'Alert source (e.g., api, datadog)',
|
||||
condition: { field: 'operation', value: 'rootly_update_alert' },
|
||||
mode: 'advanced',
|
||||
},
|
||||
{
|
||||
id: 'updateAlertServiceIds',
|
||||
title: 'Service IDs',
|
||||
type: 'short-input',
|
||||
placeholder: 'Comma-separated service IDs',
|
||||
condition: { field: 'operation', value: 'rootly_update_alert' },
|
||||
mode: 'advanced',
|
||||
},
|
||||
{
|
||||
id: 'updateAlertGroupIds',
|
||||
title: 'Team IDs',
|
||||
type: 'short-input',
|
||||
placeholder: 'Comma-separated team/group IDs',
|
||||
condition: { field: 'operation', value: 'rootly_update_alert' },
|
||||
mode: 'advanced',
|
||||
},
|
||||
{
|
||||
id: 'updateAlertEnvironmentIds',
|
||||
title: 'Environment IDs',
|
||||
type: 'short-input',
|
||||
placeholder: 'Comma-separated environment IDs',
|
||||
condition: { field: 'operation', value: 'rootly_update_alert' },
|
||||
mode: 'advanced',
|
||||
},
|
||||
{
|
||||
id: 'updateAlertExternalId',
|
||||
title: 'External ID',
|
||||
type: 'short-input',
|
||||
placeholder: 'External alert ID',
|
||||
condition: { field: 'operation', value: 'rootly_update_alert' },
|
||||
mode: 'advanced',
|
||||
},
|
||||
{
|
||||
id: 'updateAlertExternalUrl',
|
||||
title: 'External URL',
|
||||
type: 'short-input',
|
||||
placeholder: 'Link to external source',
|
||||
condition: { field: 'operation', value: 'rootly_update_alert' },
|
||||
mode: 'advanced',
|
||||
},
|
||||
{
|
||||
id: 'updateAlertDeduplicationKey',
|
||||
title: 'Deduplication Key',
|
||||
type: 'short-input',
|
||||
placeholder: 'Key to deduplicate alerts',
|
||||
condition: { field: 'operation', value: 'rootly_update_alert' },
|
||||
mode: 'advanced',
|
||||
},
|
||||
|
||||
{
|
||||
id: 'ackAlertId',
|
||||
title: 'Alert ID',
|
||||
type: 'short-input',
|
||||
placeholder: 'The ID of the alert to acknowledge',
|
||||
condition: { field: 'operation', value: 'rootly_acknowledge_alert' },
|
||||
required: { field: 'operation', value: 'rootly_acknowledge_alert' },
|
||||
},
|
||||
|
||||
{
|
||||
id: 'resolveAlertId',
|
||||
title: 'Alert ID',
|
||||
type: 'short-input',
|
||||
placeholder: 'The ID of the alert to resolve',
|
||||
condition: { field: 'operation', value: 'rootly_resolve_alert' },
|
||||
required: { field: 'operation', value: 'rootly_resolve_alert' },
|
||||
},
|
||||
{
|
||||
id: 'resolveResolutionMessage',
|
||||
title: 'Resolution Message',
|
||||
type: 'long-input',
|
||||
placeholder: 'How was the alert resolved?',
|
||||
condition: { field: 'operation', value: 'rootly_resolve_alert' },
|
||||
},
|
||||
{
|
||||
id: 'resolveRelatedIncidents',
|
||||
title: 'Resolve Related Incidents',
|
||||
type: 'dropdown',
|
||||
options: [
|
||||
{ label: 'No', id: '' },
|
||||
{ label: 'Yes', id: 'true' },
|
||||
],
|
||||
value: () => '',
|
||||
condition: { field: 'operation', value: 'rootly_resolve_alert' },
|
||||
mode: 'advanced',
|
||||
},
|
||||
|
||||
{
|
||||
id: 'actionItemIncidentId',
|
||||
title: 'Incident ID',
|
||||
type: 'short-input',
|
||||
placeholder: 'The ID of the incident',
|
||||
condition: { field: 'operation', value: 'rootly_create_action_item' },
|
||||
required: { field: 'operation', value: 'rootly_create_action_item' },
|
||||
},
|
||||
{
|
||||
id: 'actionItemSummary',
|
||||
title: 'Summary',
|
||||
type: 'short-input',
|
||||
placeholder: 'Action item title',
|
||||
condition: { field: 'operation', value: 'rootly_create_action_item' },
|
||||
required: { field: 'operation', value: 'rootly_create_action_item' },
|
||||
},
|
||||
{
|
||||
id: 'actionItemDescription',
|
||||
title: 'Description',
|
||||
type: 'long-input',
|
||||
placeholder: 'Describe the action item',
|
||||
condition: { field: 'operation', value: 'rootly_create_action_item' },
|
||||
},
|
||||
{
|
||||
id: 'actionItemKind',
|
||||
title: 'Kind',
|
||||
type: 'dropdown',
|
||||
options: [
|
||||
{ label: 'Default', id: '' },
|
||||
{ label: 'Task', id: 'task' },
|
||||
{ label: 'Follow Up', id: 'follow_up' },
|
||||
],
|
||||
value: () => '',
|
||||
condition: { field: 'operation', value: 'rootly_create_action_item' },
|
||||
},
|
||||
{
|
||||
id: 'actionItemPriority',
|
||||
title: 'Priority',
|
||||
type: 'dropdown',
|
||||
options: [
|
||||
{ label: 'Default', id: '' },
|
||||
{ label: 'High', id: 'high' },
|
||||
{ label: 'Medium', id: 'medium' },
|
||||
{ label: 'Low', id: 'low' },
|
||||
],
|
||||
value: () => '',
|
||||
condition: { field: 'operation', value: 'rootly_create_action_item' },
|
||||
},
|
||||
{
|
||||
id: 'actionItemStatus',
|
||||
title: 'Status',
|
||||
type: 'dropdown',
|
||||
options: [
|
||||
{ label: 'Default', id: '' },
|
||||
{ label: 'Open', id: 'open' },
|
||||
{ label: 'In Progress', id: 'in_progress' },
|
||||
{ label: 'Cancelled', id: 'cancelled' },
|
||||
{ label: 'Done', id: 'done' },
|
||||
],
|
||||
value: () => '',
|
||||
condition: { field: 'operation', value: 'rootly_create_action_item' },
|
||||
mode: 'advanced',
|
||||
},
|
||||
{
|
||||
id: 'actionItemAssignedToUserId',
|
||||
title: 'Assigned To User ID',
|
||||
type: 'short-input',
|
||||
placeholder: 'User ID to assign (use List Users to find IDs)',
|
||||
condition: { field: 'operation', value: 'rootly_create_action_item' },
|
||||
mode: 'advanced',
|
||||
},
|
||||
{
|
||||
id: 'actionItemDueDate',
|
||||
title: 'Due Date',
|
||||
type: 'short-input',
|
||||
placeholder: 'YYYY-MM-DD',
|
||||
condition: { field: 'operation', value: 'rootly_create_action_item' },
|
||||
mode: 'advanced',
|
||||
wandConfig: {
|
||||
enabled: true,
|
||||
prompt:
|
||||
'Generate a date in YYYY-MM-DD format for the requested due date. Return ONLY the date string - no explanations, no extra text.',
|
||||
placeholder: 'Describe the due date (e.g., "next Friday", "in 2 weeks")...',
|
||||
generationType: 'timestamp',
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
id: 'listActionItemsIncidentId',
|
||||
title: 'Incident ID',
|
||||
type: 'short-input',
|
||||
placeholder: 'The ID of the incident',
|
||||
condition: { field: 'operation', value: 'rootly_list_action_items' },
|
||||
required: { field: 'operation', value: 'rootly_list_action_items' },
|
||||
},
|
||||
{
|
||||
id: 'listActionItemsPageSize',
|
||||
title: 'Page Size',
|
||||
type: 'short-input',
|
||||
placeholder: '20',
|
||||
condition: { field: 'operation', value: 'rootly_list_action_items' },
|
||||
mode: 'advanced',
|
||||
},
|
||||
{
|
||||
id: 'listActionItemsPageNumber',
|
||||
title: 'Page Number',
|
||||
type: 'short-input',
|
||||
placeholder: '1',
|
||||
condition: { field: 'operation', value: 'rootly_list_action_items' },
|
||||
mode: 'advanced',
|
||||
},
|
||||
|
||||
{
|
||||
id: 'usersSearch',
|
||||
title: 'Search',
|
||||
type: 'short-input',
|
||||
placeholder: 'Search users...',
|
||||
condition: { field: 'operation', value: 'rootly_list_users' },
|
||||
},
|
||||
{
|
||||
id: 'usersEmail',
|
||||
title: 'Email Filter',
|
||||
type: 'short-input',
|
||||
placeholder: 'Filter by email address',
|
||||
condition: { field: 'operation', value: 'rootly_list_users' },
|
||||
mode: 'advanced',
|
||||
},
|
||||
{
|
||||
id: 'usersPageSize',
|
||||
title: 'Page Size',
|
||||
type: 'short-input',
|
||||
placeholder: '20',
|
||||
condition: { field: 'operation', value: 'rootly_list_users' },
|
||||
mode: 'advanced',
|
||||
},
|
||||
{
|
||||
id: 'usersPageNumber',
|
||||
title: 'Page Number',
|
||||
type: 'short-input',
|
||||
placeholder: '1',
|
||||
condition: { field: 'operation', value: 'rootly_list_users' },
|
||||
mode: 'advanced',
|
||||
},
|
||||
|
||||
{
|
||||
id: 'onCallsScheduleIds',
|
||||
title: 'Schedule IDs',
|
||||
type: 'short-input',
|
||||
placeholder: 'Comma-separated schedule IDs',
|
||||
condition: { field: 'operation', value: 'rootly_list_on_calls' },
|
||||
},
|
||||
{
|
||||
id: 'onCallsEscalationPolicyIds',
|
||||
title: 'Escalation Policy IDs',
|
||||
type: 'short-input',
|
||||
placeholder: 'Comma-separated escalation policy IDs',
|
||||
condition: { field: 'operation', value: 'rootly_list_on_calls' },
|
||||
},
|
||||
{
|
||||
id: 'onCallsUserIds',
|
||||
title: 'User IDs',
|
||||
type: 'short-input',
|
||||
placeholder: 'Comma-separated user IDs',
|
||||
condition: { field: 'operation', value: 'rootly_list_on_calls' },
|
||||
mode: 'advanced',
|
||||
},
|
||||
{
|
||||
id: 'onCallsServiceIds',
|
||||
title: 'Service IDs',
|
||||
type: 'short-input',
|
||||
placeholder: 'Comma-separated service IDs',
|
||||
condition: { field: 'operation', value: 'rootly_list_on_calls' },
|
||||
mode: 'advanced',
|
||||
},
|
||||
|
||||
{
|
||||
id: 'schedulesSearch',
|
||||
title: 'Search',
|
||||
type: 'short-input',
|
||||
placeholder: 'Search schedules...',
|
||||
condition: { field: 'operation', value: 'rootly_list_schedules' },
|
||||
},
|
||||
{
|
||||
id: 'schedulesPageSize',
|
||||
title: 'Page Size',
|
||||
type: 'short-input',
|
||||
placeholder: '20',
|
||||
condition: { field: 'operation', value: 'rootly_list_schedules' },
|
||||
mode: 'advanced',
|
||||
},
|
||||
{
|
||||
id: 'schedulesPageNumber',
|
||||
title: 'Page Number',
|
||||
type: 'short-input',
|
||||
placeholder: '1',
|
||||
condition: { field: 'operation', value: 'rootly_list_schedules' },
|
||||
mode: 'advanced',
|
||||
},
|
||||
|
||||
{
|
||||
id: 'escalationPoliciesSearch',
|
||||
title: 'Search',
|
||||
type: 'short-input',
|
||||
placeholder: 'Search escalation policies...',
|
||||
condition: { field: 'operation', value: 'rootly_list_escalation_policies' },
|
||||
},
|
||||
{
|
||||
id: 'escalationPoliciesPageSize',
|
||||
title: 'Page Size',
|
||||
type: 'short-input',
|
||||
placeholder: '20',
|
||||
condition: { field: 'operation', value: 'rootly_list_escalation_policies' },
|
||||
mode: 'advanced',
|
||||
},
|
||||
{
|
||||
id: 'escalationPoliciesPageNumber',
|
||||
title: 'Page Number',
|
||||
type: 'short-input',
|
||||
placeholder: '1',
|
||||
condition: { field: 'operation', value: 'rootly_list_escalation_policies' },
|
||||
mode: 'advanced',
|
||||
},
|
||||
|
||||
{
|
||||
id: 'causesSearch',
|
||||
title: 'Search',
|
||||
type: 'short-input',
|
||||
placeholder: 'Search causes...',
|
||||
condition: { field: 'operation', value: 'rootly_list_causes' },
|
||||
},
|
||||
{
|
||||
id: 'causesPageSize',
|
||||
title: 'Page Size',
|
||||
type: 'short-input',
|
||||
placeholder: '20',
|
||||
condition: { field: 'operation', value: 'rootly_list_causes' },
|
||||
mode: 'advanced',
|
||||
},
|
||||
{
|
||||
id: 'causesPageNumber',
|
||||
title: 'Page Number',
|
||||
type: 'short-input',
|
||||
placeholder: '1',
|
||||
condition: { field: 'operation', value: 'rootly_list_causes' },
|
||||
mode: 'advanced',
|
||||
},
|
||||
|
||||
{
|
||||
id: 'playbooksPageSize',
|
||||
title: 'Page Size',
|
||||
type: 'short-input',
|
||||
placeholder: '20',
|
||||
condition: { field: 'operation', value: 'rootly_list_playbooks' },
|
||||
mode: 'advanced',
|
||||
},
|
||||
{
|
||||
id: 'playbooksPageNumber',
|
||||
title: 'Page Number',
|
||||
type: 'short-input',
|
||||
placeholder: '1',
|
||||
condition: { field: 'operation', value: 'rootly_list_playbooks' },
|
||||
mode: 'advanced',
|
||||
},
|
||||
|
||||
{
|
||||
id: 'apiKey',
|
||||
title: 'API Key',
|
||||
@@ -829,6 +1225,19 @@ export const RootlyBlock: BlockConfig<RootlyResponse> = {
|
||||
'rootly_list_incident_types',
|
||||
'rootly_list_functionalities',
|
||||
'rootly_list_retrospectives',
|
||||
'rootly_delete_incident',
|
||||
'rootly_get_alert',
|
||||
'rootly_update_alert',
|
||||
'rootly_acknowledge_alert',
|
||||
'rootly_resolve_alert',
|
||||
'rootly_create_action_item',
|
||||
'rootly_list_action_items',
|
||||
'rootly_list_users',
|
||||
'rootly_list_on_calls',
|
||||
'rootly_list_schedules',
|
||||
'rootly_list_escalation_policies',
|
||||
'rootly_list_causes',
|
||||
'rootly_list_playbooks',
|
||||
],
|
||||
config: {
|
||||
tool: (params) => params.operation,
|
||||
@@ -1012,6 +1421,131 @@ export const RootlyBlock: BlockConfig<RootlyResponse> = {
|
||||
: undefined,
|
||||
}
|
||||
|
||||
case 'rootly_delete_incident':
|
||||
return {
|
||||
...baseParams,
|
||||
incidentId: params.deleteIncidentId,
|
||||
}
|
||||
|
||||
case 'rootly_get_alert':
|
||||
return {
|
||||
...baseParams,
|
||||
alertId: params.getAlertId,
|
||||
}
|
||||
|
||||
case 'rootly_update_alert':
|
||||
return {
|
||||
...baseParams,
|
||||
alertId: params.updateAlertId,
|
||||
summary: params.updateAlertSummary,
|
||||
description: params.updateAlertDescription,
|
||||
source: params.updateAlertSource,
|
||||
serviceIds: params.updateAlertServiceIds,
|
||||
groupIds: params.updateAlertGroupIds,
|
||||
environmentIds: params.updateAlertEnvironmentIds,
|
||||
externalId: params.updateAlertExternalId,
|
||||
externalUrl: params.updateAlertExternalUrl,
|
||||
deduplicationKey: params.updateAlertDeduplicationKey,
|
||||
}
|
||||
|
||||
case 'rootly_acknowledge_alert':
|
||||
return {
|
||||
...baseParams,
|
||||
alertId: params.ackAlertId,
|
||||
}
|
||||
|
||||
case 'rootly_resolve_alert':
|
||||
return {
|
||||
...baseParams,
|
||||
alertId: params.resolveAlertId,
|
||||
resolutionMessage: params.resolveResolutionMessage,
|
||||
resolveRelatedIncidents: params.resolveRelatedIncidents
|
||||
? params.resolveRelatedIncidents === 'true'
|
||||
: undefined,
|
||||
}
|
||||
|
||||
case 'rootly_create_action_item':
|
||||
return {
|
||||
...baseParams,
|
||||
incidentId: params.actionItemIncidentId,
|
||||
summary: params.actionItemSummary,
|
||||
description: params.actionItemDescription,
|
||||
kind: params.actionItemKind,
|
||||
priority: params.actionItemPriority,
|
||||
status: params.actionItemStatus,
|
||||
assignedToUserId: params.actionItemAssignedToUserId,
|
||||
dueDate: params.actionItemDueDate,
|
||||
}
|
||||
|
||||
case 'rootly_list_action_items':
|
||||
return {
|
||||
...baseParams,
|
||||
incidentId: params.listActionItemsIncidentId,
|
||||
pageSize: params.listActionItemsPageSize
|
||||
? Number(params.listActionItemsPageSize)
|
||||
: undefined,
|
||||
pageNumber: params.listActionItemsPageNumber
|
||||
? Number(params.listActionItemsPageNumber)
|
||||
: undefined,
|
||||
}
|
||||
|
||||
case 'rootly_list_users':
|
||||
return {
|
||||
...baseParams,
|
||||
search: params.usersSearch,
|
||||
email: params.usersEmail,
|
||||
pageSize: params.usersPageSize ? Number(params.usersPageSize) : undefined,
|
||||
pageNumber: params.usersPageNumber ? Number(params.usersPageNumber) : undefined,
|
||||
}
|
||||
|
||||
case 'rootly_list_on_calls':
|
||||
return {
|
||||
...baseParams,
|
||||
scheduleIds: params.onCallsScheduleIds,
|
||||
escalationPolicyIds: params.onCallsEscalationPolicyIds,
|
||||
userIds: params.onCallsUserIds,
|
||||
serviceIds: params.onCallsServiceIds,
|
||||
}
|
||||
|
||||
case 'rootly_list_schedules':
|
||||
return {
|
||||
...baseParams,
|
||||
search: params.schedulesSearch,
|
||||
pageSize: params.schedulesPageSize ? Number(params.schedulesPageSize) : undefined,
|
||||
pageNumber: params.schedulesPageNumber
|
||||
? Number(params.schedulesPageNumber)
|
||||
: undefined,
|
||||
}
|
||||
|
||||
case 'rootly_list_escalation_policies':
|
||||
return {
|
||||
...baseParams,
|
||||
search: params.escalationPoliciesSearch,
|
||||
pageSize: params.escalationPoliciesPageSize
|
||||
? Number(params.escalationPoliciesPageSize)
|
||||
: undefined,
|
||||
pageNumber: params.escalationPoliciesPageNumber
|
||||
? Number(params.escalationPoliciesPageNumber)
|
||||
: undefined,
|
||||
}
|
||||
|
||||
case 'rootly_list_causes':
|
||||
return {
|
||||
...baseParams,
|
||||
search: params.causesSearch,
|
||||
pageSize: params.causesPageSize ? Number(params.causesPageSize) : undefined,
|
||||
pageNumber: params.causesPageNumber ? Number(params.causesPageNumber) : undefined,
|
||||
}
|
||||
|
||||
case 'rootly_list_playbooks':
|
||||
return {
|
||||
...baseParams,
|
||||
pageSize: params.playbooksPageSize ? Number(params.playbooksPageSize) : undefined,
|
||||
pageNumber: params.playbooksPageNumber
|
||||
? Number(params.playbooksPageNumber)
|
||||
: undefined,
|
||||
}
|
||||
|
||||
default:
|
||||
return baseParams
|
||||
}
|
||||
@@ -1101,6 +1635,58 @@ export const RootlyBlock: BlockConfig<RootlyResponse> = {
|
||||
retrospectivesSearch: { type: 'string', description: 'Search retrospectives' },
|
||||
retrospectivesPageSize: { type: 'string', description: 'Retrospectives page size' },
|
||||
retrospectivesPageNumber: { type: 'string', description: 'Retrospectives page number' },
|
||||
deleteIncidentId: { type: 'string', description: 'Incident ID to delete' },
|
||||
getAlertId: { type: 'string', description: 'Alert ID to retrieve' },
|
||||
updateAlertId: { type: 'string', description: 'Alert ID to update' },
|
||||
updateAlertSummary: { type: 'string', description: 'Updated alert summary' },
|
||||
updateAlertDescription: { type: 'string', description: 'Updated alert description' },
|
||||
updateAlertSource: { type: 'string', description: 'Updated alert source' },
|
||||
updateAlertServiceIds: { type: 'string', description: 'Updated alert service IDs' },
|
||||
updateAlertGroupIds: { type: 'string', description: 'Updated alert team IDs' },
|
||||
updateAlertEnvironmentIds: { type: 'string', description: 'Updated alert environment IDs' },
|
||||
updateAlertExternalId: { type: 'string', description: 'Updated external alert ID' },
|
||||
updateAlertExternalUrl: { type: 'string', description: 'Updated external URL' },
|
||||
updateAlertDeduplicationKey: { type: 'string', description: 'Updated deduplication key' },
|
||||
ackAlertId: { type: 'string', description: 'Alert ID to acknowledge' },
|
||||
resolveAlertId: { type: 'string', description: 'Alert ID to resolve' },
|
||||
resolveResolutionMessage: { type: 'string', description: 'Resolution message' },
|
||||
resolveRelatedIncidents: { type: 'string', description: 'Resolve related incidents' },
|
||||
actionItemIncidentId: { type: 'string', description: 'Incident ID for action item' },
|
||||
actionItemSummary: { type: 'string', description: 'Action item summary' },
|
||||
actionItemDescription: { type: 'string', description: 'Action item description' },
|
||||
actionItemKind: { type: 'string', description: 'Action item kind' },
|
||||
actionItemPriority: { type: 'string', description: 'Action item priority' },
|
||||
actionItemStatus: { type: 'string', description: 'Action item status' },
|
||||
actionItemAssignedToUserId: { type: 'string', description: 'Assigned user ID' },
|
||||
actionItemDueDate: { type: 'string', description: 'Action item due date' },
|
||||
listActionItemsIncidentId: { type: 'string', description: 'Incident ID for action items' },
|
||||
listActionItemsPageSize: { type: 'string', description: 'Action items page size' },
|
||||
listActionItemsPageNumber: { type: 'string', description: 'Action items page number' },
|
||||
usersSearch: { type: 'string', description: 'Search users' },
|
||||
usersEmail: { type: 'string', description: 'Filter users by email' },
|
||||
usersPageSize: { type: 'string', description: 'Users page size' },
|
||||
usersPageNumber: { type: 'string', description: 'Users page number' },
|
||||
onCallsScheduleIds: { type: 'string', description: 'Filter on-calls by schedule IDs' },
|
||||
onCallsEscalationPolicyIds: {
|
||||
type: 'string',
|
||||
description: 'Filter on-calls by escalation policy IDs',
|
||||
},
|
||||
onCallsUserIds: { type: 'string', description: 'Filter on-calls by user IDs' },
|
||||
onCallsServiceIds: { type: 'string', description: 'Filter on-calls by service IDs' },
|
||||
schedulesSearch: { type: 'string', description: 'Search schedules' },
|
||||
schedulesPageSize: { type: 'string', description: 'Schedules page size' },
|
||||
schedulesPageNumber: { type: 'string', description: 'Schedules page number' },
|
||||
escalationPoliciesSearch: { type: 'string', description: 'Search escalation policies' },
|
||||
escalationPoliciesPageSize: { type: 'string', description: 'Escalation policies page size' },
|
||||
escalationPoliciesPageNumber: {
|
||||
type: 'string',
|
||||
description: 'Escalation policies page number',
|
||||
},
|
||||
causesSearch: { type: 'string', description: 'Search causes' },
|
||||
causesPageSize: { type: 'string', description: 'Causes page size' },
|
||||
causesPageNumber: { type: 'string', description: 'Causes page number' },
|
||||
playbooksPageSize: { type: 'string', description: 'Playbooks page size' },
|
||||
playbooksPageNumber: { type: 'string', description: 'Playbooks page number' },
|
||||
},
|
||||
outputs: {
|
||||
incident: {
|
||||
@@ -1150,6 +1736,42 @@ export const RootlyBlock: BlockConfig<RootlyResponse> = {
|
||||
type: 'json',
|
||||
description: 'List of retrospectives (id, title, status, url, timestamps)',
|
||||
},
|
||||
users: {
|
||||
type: 'json',
|
||||
description: 'List of users (id, name, email)',
|
||||
},
|
||||
onCalls: {
|
||||
type: 'json',
|
||||
description:
|
||||
'List of on-call entries (userId, userName, scheduleId, scheduleName, escalationPolicyId)',
|
||||
},
|
||||
schedules: {
|
||||
type: 'json',
|
||||
description: 'List of schedules (id, name, description)',
|
||||
},
|
||||
escalationPolicies: {
|
||||
type: 'json',
|
||||
description: 'List of escalation policies (id, name, description)',
|
||||
},
|
||||
causes: {
|
||||
type: 'json',
|
||||
description: 'List of causes (id, name, slug, description)',
|
||||
},
|
||||
playbooks: {
|
||||
type: 'json',
|
||||
description: 'List of playbooks (id, title, summary)',
|
||||
},
|
||||
actionItem: {
|
||||
type: 'json',
|
||||
description: 'Action item data (id, summary, description, kind, priority, status, dueDate)',
|
||||
},
|
||||
actionItems: {
|
||||
type: 'json',
|
||||
description:
|
||||
'List of action items (id, summary, description, kind, priority, status, dueDate)',
|
||||
},
|
||||
success: { type: 'boolean', description: 'Whether the operation succeeded' },
|
||||
message: { type: 'string', description: 'Operation result message' },
|
||||
totalCount: { type: 'number', description: 'Total count of items returned' },
|
||||
},
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { A2ABlock } from '@/blocks/blocks/a2a'
|
||||
import { AgentBlock } from '@/blocks/blocks/agent'
|
||||
import { AgentMailBlock } from '@/blocks/blocks/agentmail'
|
||||
import { AhrefsBlock } from '@/blocks/blocks/ahrefs'
|
||||
import { AirtableBlock } from '@/blocks/blocks/airtable'
|
||||
import { AirweaveBlock } from '@/blocks/blocks/airweave'
|
||||
@@ -25,6 +26,7 @@ import { ClerkBlock } from '@/blocks/blocks/clerk'
|
||||
import { CloudflareBlock } from '@/blocks/blocks/cloudflare'
|
||||
import { ConditionBlock } from '@/blocks/blocks/condition'
|
||||
import { ConfluenceBlock, ConfluenceV2Block } from '@/blocks/blocks/confluence'
|
||||
import { CredentialBlock } from '@/blocks/blocks/credential'
|
||||
import { CursorBlock, CursorV2Block } from '@/blocks/blocks/cursor'
|
||||
import { DatabricksBlock } from '@/blocks/blocks/databricks'
|
||||
import { DatadogBlock } from '@/blocks/blocks/datadog'
|
||||
@@ -217,6 +219,7 @@ import type { BlockConfig } from '@/blocks/types'
|
||||
export const registry: Record<string, BlockConfig> = {
|
||||
a2a: A2ABlock,
|
||||
agent: AgentBlock,
|
||||
agentmail: AgentMailBlock,
|
||||
ahrefs: AhrefsBlock,
|
||||
airtable: AirtableBlock,
|
||||
airweave: AirweaveBlock,
|
||||
@@ -241,6 +244,7 @@ export const registry: Record<string, BlockConfig> = {
|
||||
clay: ClayBlock,
|
||||
clerk: ClerkBlock,
|
||||
condition: ConditionBlock,
|
||||
credential: CredentialBlock,
|
||||
confluence: ConfluenceBlock,
|
||||
confluence_v2: ConfluenceV2Block,
|
||||
cursor: CursorBlock,
|
||||
|
||||
@@ -421,15 +421,11 @@ export interface SubBlockConfig {
|
||||
triggerId?: string
|
||||
// Dropdown/Combobox: Function to fetch options dynamically
|
||||
// Works with both 'dropdown' (select-only) and 'combobox' (editable with expression support)
|
||||
fetchOptions?: (
|
||||
blockId: string,
|
||||
subBlockId: string
|
||||
) => Promise<Array<{ label: string; id: string }>>
|
||||
fetchOptions?: (blockId: string) => Promise<Array<{ label: string; id: string }>>
|
||||
// Dropdown/Combobox: Function to fetch a single option's label by ID (for hydration)
|
||||
// Called when component mounts with a stored value to display the correct label before options load
|
||||
fetchOptionById?: (
|
||||
blockId: string,
|
||||
subBlockId: string,
|
||||
optionId: string
|
||||
) => Promise<{ label: string; id: string } | null>
|
||||
}
|
||||
|
||||
@@ -267,3 +267,24 @@ export const baseStyles = {
|
||||
margin: '8px 0',
|
||||
},
|
||||
}
|
||||
|
||||
/** Styles for plain personal emails (no branding, no EmailLayout) */
|
||||
export const plainEmailStyles = {
|
||||
body: {
|
||||
fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif',
|
||||
backgroundColor: '#ffffff',
|
||||
margin: '0',
|
||||
padding: '0',
|
||||
},
|
||||
container: {
|
||||
maxWidth: '560px',
|
||||
margin: '40px auto',
|
||||
padding: '0 24px',
|
||||
},
|
||||
p: {
|
||||
fontSize: '15px',
|
||||
lineHeight: '1.6',
|
||||
color: '#1a1a1a',
|
||||
margin: '0 0 16px',
|
||||
},
|
||||
} as const
|
||||
|
||||
@@ -1 +1 @@
|
||||
export { baseStyles, colors, spacing, typography } from './base'
|
||||
export { baseStyles, colors, plainEmailStyles, spacing, typography } from './base'
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
export { OnboardingFollowupEmail } from './onboarding-followup-email'
|
||||
export { OTPVerificationEmail } from './otp-verification-email'
|
||||
export { ResetPasswordEmail } from './reset-password-email'
|
||||
export { WelcomeEmail } from './welcome-email'
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
import { Body, Head, Html, Preview, Text } from '@react-email/components'
|
||||
import { plainEmailStyles as styles } from '@/components/emails/_styles'
|
||||
|
||||
interface OnboardingFollowupEmailProps {
|
||||
userName?: string
|
||||
}
|
||||
|
||||
export function OnboardingFollowupEmail({ userName }: OnboardingFollowupEmailProps) {
|
||||
return (
|
||||
<Html>
|
||||
<Head />
|
||||
<Preview>Quick question</Preview>
|
||||
<Body style={styles.body}>
|
||||
<div style={styles.container}>
|
||||
<Text style={styles.p}>{userName ? `Hey ${userName},` : 'Hey,'}</Text>
|
||||
<Text style={styles.p}>
|
||||
It's been a few days since you signed up. I hope you're enjoying Sim!
|
||||
</Text>
|
||||
<Text style={styles.p}>
|
||||
I'd love to know — what did you expect when you signed up vs. what did you get?
|
||||
</Text>
|
||||
<Text style={styles.p}>
|
||||
A reply with your thoughts would really help us improve the product for everyone.
|
||||
</Text>
|
||||
<Text style={styles.p}>
|
||||
Thanks,
|
||||
<br />
|
||||
Emir
|
||||
<br />
|
||||
Founder, Sim
|
||||
</Text>
|
||||
</div>
|
||||
</Body>
|
||||
</Html>
|
||||
)
|
||||
}
|
||||
|
||||
export default OnboardingFollowupEmail
|
||||
@@ -0,0 +1,33 @@
|
||||
import { Body, Head, Html, Preview, Text } from '@react-email/components'
|
||||
import { plainEmailStyles as styles } from '@/components/emails/_styles'
|
||||
|
||||
interface AbandonedCheckoutEmailProps {
|
||||
userName?: string
|
||||
}
|
||||
|
||||
export function AbandonedCheckoutEmail({ userName }: AbandonedCheckoutEmailProps) {
|
||||
return (
|
||||
<Html>
|
||||
<Head />
|
||||
<Preview>Did you run into an issue with your upgrade?</Preview>
|
||||
<Body style={styles.body}>
|
||||
<div style={styles.container}>
|
||||
<Text style={styles.p}>{userName ? `Hi ${userName},` : 'Hi,'}</Text>
|
||||
<Text style={styles.p}>
|
||||
I saw that you tried to upgrade your Sim plan but didn't end up completing it.
|
||||
</Text>
|
||||
<Text style={styles.p}>
|
||||
Did you run into an issue, or did you have a question? Here to help.
|
||||
</Text>
|
||||
<Text style={styles.p}>
|
||||
— Emir
|
||||
<br />
|
||||
Founder, Sim
|
||||
</Text>
|
||||
</div>
|
||||
</Body>
|
||||
</Html>
|
||||
)
|
||||
}
|
||||
|
||||
export default AbandonedCheckoutEmail
|
||||
7
apps/sim/components/emails/billing/constants.ts
Normal file
7
apps/sim/components/emails/billing/constants.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
/** Pro plan features shown in billing upgrade emails */
|
||||
export const proFeatures = [
|
||||
{ label: '6,000 credits/month', desc: 'included' },
|
||||
{ label: '+50 daily refresh', desc: 'credits per day' },
|
||||
{ label: '150 runs/min', desc: 'sync executions' },
|
||||
{ label: '50GB storage', desc: 'for files & assets' },
|
||||
] as const
|
||||
102
apps/sim/components/emails/billing/credits-exhausted-email.tsx
Normal file
102
apps/sim/components/emails/billing/credits-exhausted-email.tsx
Normal file
@@ -0,0 +1,102 @@
|
||||
import { Link, Section, Text } from '@react-email/components'
|
||||
import { baseStyles, colors, typography } from '@/components/emails/_styles'
|
||||
import { proFeatures } from '@/components/emails/billing/constants'
|
||||
import { EmailLayout } from '@/components/emails/components'
|
||||
import { dollarsToCredits } from '@/lib/billing/credits/conversion'
|
||||
import { getBrandConfig } from '@/ee/whitelabeling'
|
||||
|
||||
interface CreditsExhaustedEmailProps {
|
||||
userName?: string
|
||||
limit: number
|
||||
upgradeLink: string
|
||||
}
|
||||
|
||||
export function CreditsExhaustedEmail({
|
||||
userName,
|
||||
limit,
|
||||
upgradeLink,
|
||||
}: CreditsExhaustedEmailProps) {
|
||||
const brand = getBrandConfig()
|
||||
|
||||
return (
|
||||
<EmailLayout
|
||||
preview={`You've used all ${dollarsToCredits(limit).toLocaleString()} of your free ${brand.name} credits`}
|
||||
showUnsubscribe={true}
|
||||
>
|
||||
<Text style={{ ...baseStyles.paragraph, marginTop: 0 }}>
|
||||
{userName ? `Hi ${userName},` : 'Hi,'}
|
||||
</Text>
|
||||
|
||||
<Text style={baseStyles.paragraph}>
|
||||
You've used all <strong>{dollarsToCredits(limit).toLocaleString()}</strong> of your
|
||||
free credits on {brand.name}. Your workflows are paused until you upgrade.
|
||||
</Text>
|
||||
|
||||
<Section
|
||||
style={{
|
||||
backgroundColor: '#f8faf9',
|
||||
border: `1px solid ${colors.brandTertiary}20`,
|
||||
borderRadius: '8px',
|
||||
padding: '16px 20px',
|
||||
margin: '16px 0',
|
||||
}}
|
||||
>
|
||||
<Text
|
||||
style={{
|
||||
fontSize: '14px',
|
||||
fontWeight: 600,
|
||||
color: colors.brandTertiary,
|
||||
fontFamily: typography.fontFamily,
|
||||
margin: '0 0 12px 0',
|
||||
textTransform: 'uppercase' as const,
|
||||
letterSpacing: '0.5px',
|
||||
}}
|
||||
>
|
||||
Pro includes
|
||||
</Text>
|
||||
<table style={{ width: '100%', borderCollapse: 'collapse' }}>
|
||||
<tbody>
|
||||
{proFeatures.map((feature, i) => (
|
||||
<tr key={i}>
|
||||
<td
|
||||
style={{
|
||||
padding: '6px 0',
|
||||
fontSize: '15px',
|
||||
fontWeight: 600,
|
||||
color: colors.textPrimary,
|
||||
fontFamily: typography.fontFamily,
|
||||
width: '45%',
|
||||
}}
|
||||
>
|
||||
{feature.label}
|
||||
</td>
|
||||
<td
|
||||
style={{
|
||||
padding: '6px 0',
|
||||
fontSize: '14px',
|
||||
color: colors.textMuted,
|
||||
fontFamily: typography.fontFamily,
|
||||
}}
|
||||
>
|
||||
{feature.desc}
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</Section>
|
||||
|
||||
<Link href={upgradeLink} style={{ textDecoration: 'none' }}>
|
||||
<Text style={baseStyles.button}>Upgrade to Pro</Text>
|
||||
</Link>
|
||||
|
||||
<div style={baseStyles.divider} />
|
||||
|
||||
<Text style={{ ...baseStyles.footerText, textAlign: 'left' }}>
|
||||
One-time notification when free credits are exhausted.
|
||||
</Text>
|
||||
</EmailLayout>
|
||||
)
|
||||
}
|
||||
|
||||
export default CreditsExhaustedEmail
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Link, Section, Text } from '@react-email/components'
|
||||
import { baseStyles, colors, typography } from '@/components/emails/_styles'
|
||||
import { proFeatures } from '@/components/emails/billing/constants'
|
||||
import { EmailLayout } from '@/components/emails/components'
|
||||
import { dollarsToCredits } from '@/lib/billing/credits/conversion'
|
||||
import { getBrandConfig } from '@/ee/whitelabeling'
|
||||
@@ -12,13 +13,6 @@ interface FreeTierUpgradeEmailProps {
|
||||
upgradeLink: string
|
||||
}
|
||||
|
||||
const proFeatures = [
|
||||
{ label: '6,000 credits/month', desc: 'included' },
|
||||
{ label: '+50 daily refresh', desc: 'credits per day' },
|
||||
{ label: '150 runs/min', desc: 'sync executions' },
|
||||
{ label: '50GB storage', desc: 'for files & assets' },
|
||||
]
|
||||
|
||||
export function FreeTierUpgradeEmail({
|
||||
userName,
|
||||
percentUsed,
|
||||
@@ -105,7 +99,7 @@ export function FreeTierUpgradeEmail({
|
||||
<div style={baseStyles.divider} />
|
||||
|
||||
<Text style={{ ...baseStyles.footerText, textAlign: 'left' }}>
|
||||
One-time notification at 90% usage.
|
||||
One-time notification at 80% usage.
|
||||
</Text>
|
||||
</EmailLayout>
|
||||
)
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
export { AbandonedCheckoutEmail } from './abandoned-checkout-email'
|
||||
export { CreditPurchaseEmail } from './credit-purchase-email'
|
||||
export { CreditsExhaustedEmail } from './credits-exhausted-email'
|
||||
export { EnterpriseSubscriptionEmail } from './enterprise-subscription-email'
|
||||
export { FreeTierUpgradeEmail } from './free-tier-upgrade-email'
|
||||
export { PaymentFailedEmail } from './payment-failed-email'
|
||||
|
||||
@@ -41,8 +41,9 @@ export function EmailLayout({
|
||||
{/* Header with logo */}
|
||||
<Section style={baseStyles.header}>
|
||||
<Img
|
||||
src={brand.logoUrl || `${baseUrl}/brand/color/email/type.png`}
|
||||
width='70'
|
||||
src={brand.logoUrl || `${baseUrl}/brand/color/email/wordmark.png`}
|
||||
width='107'
|
||||
height='33'
|
||||
alt={brand.name}
|
||||
style={{ display: 'block' }}
|
||||
/>
|
||||
|
||||
@@ -1,7 +1,14 @@
|
||||
import { render } from '@react-email/components'
|
||||
import { OTPVerificationEmail, ResetPasswordEmail, WelcomeEmail } from '@/components/emails/auth'
|
||||
import {
|
||||
OnboardingFollowupEmail,
|
||||
OTPVerificationEmail,
|
||||
ResetPasswordEmail,
|
||||
WelcomeEmail,
|
||||
} from '@/components/emails/auth'
|
||||
import {
|
||||
AbandonedCheckoutEmail,
|
||||
CreditPurchaseEmail,
|
||||
CreditsExhaustedEmail,
|
||||
EnterpriseSubscriptionEmail,
|
||||
FreeTierUpgradeEmail,
|
||||
PaymentFailedEmail,
|
||||
@@ -159,6 +166,22 @@ export async function renderWelcomeEmail(userName?: string): Promise<string> {
|
||||
return await render(WelcomeEmail({ userName }))
|
||||
}
|
||||
|
||||
export async function renderOnboardingFollowupEmail(userName?: string): Promise<string> {
|
||||
return await render(OnboardingFollowupEmail({ userName }))
|
||||
}
|
||||
|
||||
export async function renderAbandonedCheckoutEmail(userName?: string): Promise<string> {
|
||||
return await render(AbandonedCheckoutEmail({ userName }))
|
||||
}
|
||||
|
||||
export async function renderCreditsExhaustedEmail(params: {
|
||||
userName?: string
|
||||
limit: number
|
||||
upgradeLink: string
|
||||
}): Promise<string> {
|
||||
return await render(CreditsExhaustedEmail(params))
|
||||
}
|
||||
|
||||
export async function renderCreditPurchaseEmail(params: {
|
||||
userName?: string
|
||||
amount: number
|
||||
|
||||
@@ -16,6 +16,9 @@ export type EmailSubjectType =
|
||||
| 'plan-welcome-pro'
|
||||
| 'plan-welcome-team'
|
||||
| 'credit-purchase'
|
||||
| 'abandoned-checkout'
|
||||
| 'free-tier-exhausted'
|
||||
| 'onboarding-followup'
|
||||
| 'welcome'
|
||||
|
||||
/**
|
||||
@@ -48,13 +51,19 @@ export function getEmailSubject(type: EmailSubjectType): string {
|
||||
case 'usage-threshold':
|
||||
return `You're nearing your monthly budget on ${brandName}`
|
||||
case 'free-tier-upgrade':
|
||||
return `You're at 90% of your free credits on ${brandName}`
|
||||
return `You're at 80% of your free credits on ${brandName}`
|
||||
case 'plan-welcome-pro':
|
||||
return `Your Pro plan is now active on ${brandName}`
|
||||
case 'plan-welcome-team':
|
||||
return `Your Team plan is now active on ${brandName}`
|
||||
case 'credit-purchase':
|
||||
return `Credits added to your ${brandName} account`
|
||||
case 'abandoned-checkout':
|
||||
return `Quick question`
|
||||
case 'free-tier-exhausted':
|
||||
return `You've run out of free credits on ${brandName}`
|
||||
case 'onboarding-followup':
|
||||
return `Quick question about ${brandName}`
|
||||
case 'welcome':
|
||||
return `Welcome to ${brandName}`
|
||||
default:
|
||||
|
||||
@@ -1,6 +1,33 @@
|
||||
import type { SVGProps } from 'react'
|
||||
import { useId } from 'react'
|
||||
|
||||
export function AgentMailIcon(props: SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
<svg {...props} viewBox='0 0 350 363' fill='none' xmlns='http://www.w3.org/2000/svg'>
|
||||
<path
|
||||
d='M318.029 88.3407C196.474 115.33 153.48 115.321 33.9244 88.3271C30.6216 87.5814 27.1432 88.9727 25.3284 91.8313L1.24109 129.774C-1.76483 134.509 0.965276 140.798 6.46483 141.898C152.613 171.13 197.678 171.182 343.903 141.835C349.304 140.751 352.064 134.641 349.247 129.907L326.719 92.0479C324.95 89.0744 321.407 87.5907 318.029 88.3407Z'
|
||||
fill='currentColor'
|
||||
/>
|
||||
<path
|
||||
d='M75.9931 246.6L149.939 311.655C151.973 313.444 151.633 316.969 149.281 318.48L119.141 337.84C117.283 339.034 114.951 338.412 113.933 336.452L70.1276 252.036C68.0779 248.086 72.7553 243.751 75.9931 246.6Z'
|
||||
fill='currentColor'
|
||||
/>
|
||||
<path
|
||||
d='M274.025 246.6L200.08 311.655C198.046 313.444 198.385 316.969 200.737 318.48L230.877 337.84C232.736 339.034 235.068 338.412 236.085 336.452L279.891 252.036C281.941 248.086 277.263 243.751 274.025 246.6Z'
|
||||
fill='currentColor'
|
||||
/>
|
||||
<path
|
||||
d='M138.75 198.472L152.436 192.983C155.238 191.918 157.77 191.918 158.574 191.918C164.115 192.126 169.564 192.232 175.009 192.235C180.454 192.232 185.904 192.126 191.444 191.918C192.248 191.918 194.78 191.918 197.583 192.983L211.269 198.472C212.645 199.025 214.082 199.382 215.544 199.448C218.585 199.587 221.733 199.464 224.63 198.811C225.706 198.568 226.728 198.103 227.704 197.545L243.046 188.784C244.81 187.777 246.726 187.138 248.697 186.9L258.276 185.5H259.242H263.556L262.713 190.965L256.679 234.22C255.957 238.31 254.25 242.328 250.443 245.834L187.376 299.258C184.555 301.648 181.107 302.942 177.562 302.942H175.009H172.457C168.911 302.942 165.464 301.648 162.643 299.258L99.5761 245.834C95.7684 242.328 94.0614 238.31 93.3393 234.22L87.3059 190.965L86.4624 185.5H90.7771H91.7429L101.322 186.9C103.293 187.138 105.208 187.777 106.972 188.784L122.314 197.545C123.291 198.103 124.313 198.568 125.389 198.811C128.286 199.464 131.434 199.587 134.474 199.448C135.936 199.382 137.373 199.025 138.75 198.472Z'
|
||||
fill='currentColor'
|
||||
/>
|
||||
<path
|
||||
d='M102.47 0.847827C205.434 44.796 156.456 42.1015 248.434 1.63153C252.885 -1.09955 258.353 1.88915 259.419 7.69219L269.235 61.1686L270.819 69.7893L263.592 71.8231L263.582 71.8259C190.588 92.3069 165.244 92.0078 86.7576 71.7428L79.1971 69.7905L80.9925 60.8681L91.8401 6.91975C92.9559 1.3706 98.105 -1.55777 102.47 0.847827Z'
|
||||
fill='currentColor'
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
export function SearchIcon(props: SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
<svg
|
||||
@@ -97,6 +124,29 @@ export function ConditionalIcon(props: SVGProps<SVGSVGElement>) {
|
||||
)
|
||||
}
|
||||
|
||||
export function CredentialIcon(props: SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
<svg {...props} viewBox='0 0 24 24' fill='none' xmlns='http://www.w3.org/2000/svg'>
|
||||
<circle cx='8' cy='15' r='4' stroke='currentColor' strokeWidth='1.75' />
|
||||
<path d='M11.83 13.17L20 5' stroke='currentColor' strokeWidth='1.75' strokeLinecap='round' />
|
||||
<path
|
||||
d='M18 7l2 2'
|
||||
stroke='currentColor'
|
||||
strokeWidth='1.75'
|
||||
strokeLinecap='round'
|
||||
strokeLinejoin='round'
|
||||
/>
|
||||
<path
|
||||
d='M15 10l2 2'
|
||||
stroke='currentColor'
|
||||
strokeWidth='1.75'
|
||||
strokeLinecap='round'
|
||||
strokeLinejoin='round'
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
export function NoteIcon(props: SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
<svg
|
||||
|
||||
@@ -35,6 +35,8 @@ export enum BlockType {
|
||||
WORKFLOW = 'workflow',
|
||||
WORKFLOW_INPUT = 'workflow_input',
|
||||
|
||||
CREDENTIAL = 'credential',
|
||||
|
||||
WAIT = 'wait',
|
||||
|
||||
NOTE = 'note',
|
||||
|
||||
@@ -21,6 +21,7 @@ vi.mock('@/lib/core/config/feature-flags', () => ({
|
||||
isEmailVerificationEnabled: false,
|
||||
isBillingEnabled: false,
|
||||
isOrganizationsEnabled: false,
|
||||
isAccessControlEnabled: false,
|
||||
}))
|
||||
|
||||
vi.mock('@/providers/utils', () => ({
|
||||
@@ -110,6 +111,12 @@ vi.mock('@sim/db/schema', () => ({
|
||||
},
|
||||
}))
|
||||
|
||||
const mockGetCustomToolById = vi.fn()
|
||||
|
||||
vi.mock('@/lib/workflows/custom-tools/operations', () => ({
|
||||
getCustomToolById: (...args: unknown[]) => mockGetCustomToolById(...args),
|
||||
}))
|
||||
|
||||
setupGlobalFetchMock()
|
||||
|
||||
const mockGetAllBlocks = getAllBlocks as Mock
|
||||
@@ -1957,49 +1964,22 @@ describe('AgentBlockHandler', () => {
|
||||
const staleInlineCode = 'return { title, content };'
|
||||
const dbCode = 'return { title, content, format };'
|
||||
|
||||
function mockFetchForCustomTool(toolId: string) {
|
||||
mockFetch.mockImplementation((url: string) => {
|
||||
if (typeof url === 'string' && url.includes('/api/tools/custom')) {
|
||||
function mockDBForCustomTool(toolId: string) {
|
||||
mockGetCustomToolById.mockImplementation(({ toolId: id }: { toolId: string }) => {
|
||||
if (id === toolId) {
|
||||
return Promise.resolve({
|
||||
ok: true,
|
||||
headers: { get: () => null },
|
||||
json: () =>
|
||||
Promise.resolve({
|
||||
data: [
|
||||
{
|
||||
id: toolId,
|
||||
title: 'formatReport',
|
||||
schema: dbSchema,
|
||||
code: dbCode,
|
||||
},
|
||||
],
|
||||
}),
|
||||
id: toolId,
|
||||
title: 'formatReport',
|
||||
schema: dbSchema,
|
||||
code: dbCode,
|
||||
})
|
||||
}
|
||||
return Promise.resolve({
|
||||
ok: true,
|
||||
headers: { get: () => null },
|
||||
json: () => Promise.resolve({}),
|
||||
})
|
||||
return Promise.resolve(null)
|
||||
})
|
||||
}
|
||||
|
||||
function mockFetchFailure() {
|
||||
mockFetch.mockImplementation((url: string) => {
|
||||
if (typeof url === 'string' && url.includes('/api/tools/custom')) {
|
||||
return Promise.resolve({
|
||||
ok: false,
|
||||
status: 500,
|
||||
headers: { get: () => null },
|
||||
json: () => Promise.resolve({}),
|
||||
})
|
||||
}
|
||||
return Promise.resolve({
|
||||
ok: true,
|
||||
headers: { get: () => null },
|
||||
json: () => Promise.resolve({}),
|
||||
})
|
||||
})
|
||||
function mockDBFailure() {
|
||||
mockGetCustomToolById.mockRejectedValue(new Error('DB connection failed'))
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
@@ -2008,11 +1988,13 @@ describe('AgentBlockHandler', () => {
|
||||
writable: true,
|
||||
configurable: true,
|
||||
})
|
||||
mockGetCustomToolById.mockReset()
|
||||
mockContext.userId = 'test-user'
|
||||
})
|
||||
|
||||
it('should always fetch latest schema from DB when customToolId is present', async () => {
|
||||
const toolId = 'custom-tool-123'
|
||||
mockFetchForCustomTool(toolId)
|
||||
mockDBForCustomTool(toolId)
|
||||
|
||||
const inputs = {
|
||||
model: 'gpt-4o',
|
||||
@@ -2046,7 +2028,7 @@ describe('AgentBlockHandler', () => {
|
||||
|
||||
it('should fetch from DB when customToolId has no inline schema', async () => {
|
||||
const toolId = 'custom-tool-123'
|
||||
mockFetchForCustomTool(toolId)
|
||||
mockDBForCustomTool(toolId)
|
||||
|
||||
const inputs = {
|
||||
model: 'gpt-4o',
|
||||
@@ -2075,7 +2057,7 @@ describe('AgentBlockHandler', () => {
|
||||
})
|
||||
|
||||
it('should fall back to inline schema when DB fetch fails and inline exists', async () => {
|
||||
mockFetchFailure()
|
||||
mockDBFailure()
|
||||
|
||||
const inputs = {
|
||||
model: 'gpt-4o',
|
||||
@@ -2107,7 +2089,7 @@ describe('AgentBlockHandler', () => {
|
||||
})
|
||||
|
||||
it('should return null when DB fetch fails and no inline schema exists', async () => {
|
||||
mockFetchFailure()
|
||||
mockDBFailure()
|
||||
|
||||
const inputs = {
|
||||
model: 'gpt-4o',
|
||||
@@ -2135,7 +2117,7 @@ describe('AgentBlockHandler', () => {
|
||||
|
||||
it('should use DB schema when customToolId resolves', async () => {
|
||||
const toolId = 'custom-tool-123'
|
||||
mockFetchForCustomTool(toolId)
|
||||
mockDBForCustomTool(toolId)
|
||||
|
||||
const inputs = {
|
||||
model: 'gpt-4o',
|
||||
@@ -2185,10 +2167,7 @@ describe('AgentBlockHandler', () => {
|
||||
|
||||
await handler.execute(mockContext, mockBlock, inputs)
|
||||
|
||||
const customToolFetches = mockFetch.mock.calls.filter(
|
||||
(call: any[]) => typeof call[0] === 'string' && call[0].includes('/api/tools/custom')
|
||||
)
|
||||
expect(customToolFetches.length).toBe(0)
|
||||
expect(mockGetCustomToolById).not.toHaveBeenCalled()
|
||||
|
||||
expect(mockExecuteProviderRequest).toHaveBeenCalled()
|
||||
const providerCall = mockExecuteProviderRequest.mock.calls[0]
|
||||
|
||||
@@ -3,6 +3,7 @@ import { mcpServers } from '@sim/db/schema'
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { and, eq, inArray, isNull } from 'drizzle-orm'
|
||||
import { createMcpToolId } from '@/lib/mcp/utils'
|
||||
import { getCustomToolById } from '@/lib/workflows/custom-tools/operations'
|
||||
import { getAllBlocks } from '@/blocks'
|
||||
import type { BlockOutput } from '@/blocks/types'
|
||||
import {
|
||||
@@ -277,39 +278,18 @@ export class AgentBlockHandler implements BlockHandler {
|
||||
ctx: ExecutionContext,
|
||||
customToolId: string
|
||||
): Promise<{ schema: any; title: string } | null> {
|
||||
if (!ctx.userId) {
|
||||
logger.error('Cannot fetch custom tool without userId:', { customToolId })
|
||||
return null
|
||||
}
|
||||
|
||||
try {
|
||||
const headers = await buildAuthHeaders(ctx.userId)
|
||||
const params: Record<string, string> = {}
|
||||
|
||||
if (ctx.workspaceId) {
|
||||
params.workspaceId = ctx.workspaceId
|
||||
}
|
||||
if (ctx.workflowId) {
|
||||
params.workflowId = ctx.workflowId
|
||||
}
|
||||
if (ctx.userId) {
|
||||
params.userId = ctx.userId
|
||||
}
|
||||
|
||||
const url = buildAPIUrl('/api/tools/custom', params)
|
||||
const response = await fetch(url.toString(), {
|
||||
method: 'GET',
|
||||
headers,
|
||||
const tool = await getCustomToolById({
|
||||
toolId: customToolId,
|
||||
userId: ctx.userId,
|
||||
workspaceId: ctx.workspaceId,
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
await response.text().catch(() => {})
|
||||
logger.error(`Failed to fetch custom tools: ${response.status}`)
|
||||
return null
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
if (!data.data || !Array.isArray(data.data)) {
|
||||
logger.error('Invalid custom tools API response')
|
||||
return null
|
||||
}
|
||||
|
||||
const tool = data.data.find((t: any) => t.id === customToolId)
|
||||
if (!tool) {
|
||||
logger.warn(`Custom tool not found by ID: ${customToolId}`)
|
||||
return null
|
||||
|
||||
111
apps/sim/executor/handlers/credential/credential-handler.ts
Normal file
111
apps/sim/executor/handlers/credential/credential-handler.ts
Normal file
@@ -0,0 +1,111 @@
|
||||
import { db } from '@sim/db'
|
||||
import { credential } from '@sim/db/schema'
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { and, asc, eq, inArray } from 'drizzle-orm'
|
||||
import type { BlockOutput } from '@/blocks/types'
|
||||
import { BlockType } from '@/executor/constants'
|
||||
import type { BlockHandler, ExecutionContext } from '@/executor/types'
|
||||
import type { SerializedBlock } from '@/serializer/types'
|
||||
|
||||
const logger = createLogger('CredentialBlockHandler')
|
||||
|
||||
export class CredentialBlockHandler implements BlockHandler {
|
||||
canHandle(block: SerializedBlock): boolean {
|
||||
return block.metadata?.id === BlockType.CREDENTIAL
|
||||
}
|
||||
|
||||
async execute(
|
||||
ctx: ExecutionContext,
|
||||
_block: SerializedBlock,
|
||||
inputs: Record<string, unknown>
|
||||
): Promise<BlockOutput> {
|
||||
if (!ctx.workspaceId) {
|
||||
throw new Error('workspaceId is required for credential resolution')
|
||||
}
|
||||
|
||||
const operation = typeof inputs.operation === 'string' ? inputs.operation : 'select'
|
||||
|
||||
if (operation === 'list') {
|
||||
return this.listCredentials(ctx.workspaceId, inputs)
|
||||
}
|
||||
|
||||
return this.selectCredential(ctx.workspaceId, inputs)
|
||||
}
|
||||
|
||||
private async selectCredential(
|
||||
workspaceId: string,
|
||||
inputs: Record<string, unknown>
|
||||
): Promise<BlockOutput> {
|
||||
const credentialId = typeof inputs.credentialId === 'string' ? inputs.credentialId.trim() : ''
|
||||
|
||||
if (!credentialId) {
|
||||
throw new Error('No credential selected')
|
||||
}
|
||||
|
||||
const record = await db.query.credential.findFirst({
|
||||
where: and(
|
||||
eq(credential.id, credentialId),
|
||||
eq(credential.workspaceId, workspaceId),
|
||||
eq(credential.type, 'oauth')
|
||||
),
|
||||
columns: {
|
||||
id: true,
|
||||
displayName: true,
|
||||
providerId: true,
|
||||
},
|
||||
})
|
||||
|
||||
if (!record) {
|
||||
throw new Error(`Credential not found: ${credentialId}`)
|
||||
}
|
||||
|
||||
logger.info('Credential block resolved', { credentialId: record.id })
|
||||
|
||||
return {
|
||||
credentialId: record.id,
|
||||
displayName: record.displayName,
|
||||
providerId: record.providerId ?? '',
|
||||
}
|
||||
}
|
||||
|
||||
private async listCredentials(
|
||||
workspaceId: string,
|
||||
inputs: Record<string, unknown>
|
||||
): Promise<BlockOutput> {
|
||||
const providerFilter = Array.isArray(inputs.providerFilter)
|
||||
? (inputs.providerFilter as string[]).filter(Boolean)
|
||||
: []
|
||||
|
||||
const conditions = [eq(credential.workspaceId, workspaceId), eq(credential.type, 'oauth')]
|
||||
|
||||
if (providerFilter.length > 0) {
|
||||
conditions.push(inArray(credential.providerId, providerFilter))
|
||||
}
|
||||
|
||||
const records = await db.query.credential.findMany({
|
||||
where: and(...conditions),
|
||||
columns: {
|
||||
id: true,
|
||||
displayName: true,
|
||||
providerId: true,
|
||||
},
|
||||
orderBy: [asc(credential.displayName)],
|
||||
})
|
||||
|
||||
const credentials = records.map((r) => ({
|
||||
credentialId: r.id,
|
||||
displayName: r.displayName,
|
||||
providerId: r.providerId ?? '',
|
||||
}))
|
||||
|
||||
logger.info('Credential block listed credentials', {
|
||||
count: credentials.length,
|
||||
providerFilter: providerFilter.length > 0 ? providerFilter : undefined,
|
||||
})
|
||||
|
||||
return {
|
||||
credentials,
|
||||
count: credentials.length,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -8,6 +8,7 @@
|
||||
import { AgentBlockHandler } from '@/executor/handlers/agent/agent-handler'
|
||||
import { ApiBlockHandler } from '@/executor/handlers/api/api-handler'
|
||||
import { ConditionBlockHandler } from '@/executor/handlers/condition/condition-handler'
|
||||
import { CredentialBlockHandler } from '@/executor/handlers/credential/credential-handler'
|
||||
import { EvaluatorBlockHandler } from '@/executor/handlers/evaluator/evaluator-handler'
|
||||
import { FunctionBlockHandler } from '@/executor/handlers/function/function-handler'
|
||||
import { GenericBlockHandler } from '@/executor/handlers/generic/generic-handler'
|
||||
@@ -42,6 +43,7 @@ export function createBlockHandlers(): BlockHandler[] {
|
||||
new WorkflowBlockHandler(),
|
||||
new WaitBlockHandler(),
|
||||
new EvaluatorBlockHandler(),
|
||||
new CredentialBlockHandler(),
|
||||
new GenericBlockHandler(),
|
||||
]
|
||||
}
|
||||
|
||||
@@ -89,6 +89,7 @@ interface CreateApiKeyParams {
|
||||
workspaceId: string
|
||||
name: string
|
||||
keyType: 'personal' | 'workspace'
|
||||
source?: 'settings' | 'deploy_modal'
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -98,16 +99,19 @@ export function useCreateApiKey() {
|
||||
const queryClient = useQueryClient()
|
||||
|
||||
return useMutation({
|
||||
mutationFn: async ({ workspaceId, name, keyType }: CreateApiKeyParams) => {
|
||||
mutationFn: async ({ workspaceId, name, keyType, source }: CreateApiKeyParams) => {
|
||||
const url =
|
||||
keyType === 'workspace'
|
||||
? `/api/workspaces/${workspaceId}/api-keys`
|
||||
: '/api/users/me/api-keys'
|
||||
|
||||
const body: Record<string, unknown> = { name: name.trim() }
|
||||
if (keyType === 'workspace' && source) body.source = source
|
||||
|
||||
const response = await fetch(url, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ name: name.trim() }),
|
||||
body: JSON.stringify(body),
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
|
||||
@@ -73,7 +73,7 @@ export const workspaceCredentialKeys = {
|
||||
* Fetch workspace credential list from API.
|
||||
* Used by the prefetch function for hover-based cache warming.
|
||||
*/
|
||||
async function fetchWorkspaceCredentialList(
|
||||
export async function fetchWorkspaceCredentialList(
|
||||
workspaceId: string,
|
||||
signal?: AbortSignal
|
||||
): Promise<WorkspaceCredential[]> {
|
||||
|
||||
@@ -2,6 +2,7 @@ import { useQuery } from '@tanstack/react-query'
|
||||
import type { Credential } from '@/lib/oauth'
|
||||
import { CREDENTIAL_SET } from '@/executor/constants'
|
||||
import { useCredentialSetDetail } from '@/hooks/queries/credential-sets'
|
||||
import { useWorkspaceCredential } from '@/hooks/queries/credentials'
|
||||
import { fetchJson } from '@/hooks/selectors/helpers'
|
||||
|
||||
interface CredentialListResponse {
|
||||
@@ -163,17 +164,26 @@ export function useCredentialName(
|
||||
shouldFetchDetail
|
||||
)
|
||||
|
||||
// Fallback for credential blocks that have no serviceId/providerId — look up by ID directly
|
||||
const { data: workspaceCredential, isFetching: workspaceCredentialLoading } =
|
||||
useWorkspaceCredential(!providerId && !isCredentialSet ? credentialId : undefined)
|
||||
|
||||
const detailCredential = foreignCredentials[0]
|
||||
const hasForeignMeta = foreignCredentials.length > 0
|
||||
|
||||
const displayName =
|
||||
credentialSetData?.name ?? selectedCredential?.name ?? detailCredential?.name ?? null
|
||||
credentialSetData?.name ??
|
||||
selectedCredential?.name ??
|
||||
detailCredential?.name ??
|
||||
workspaceCredential?.displayName ??
|
||||
null
|
||||
|
||||
return {
|
||||
displayName,
|
||||
isLoading:
|
||||
credentialsLoading ||
|
||||
foreignLoading ||
|
||||
workspaceCredentialLoading ||
|
||||
(isCredentialSet && credentialSetLoading && !credentialSetData),
|
||||
hasForeignMeta,
|
||||
}
|
||||
|
||||
@@ -147,6 +147,14 @@ async function initializeOpenTelemetry() {
|
||||
} catch (err) {
|
||||
logger.error('Error shutting down OpenTelemetry SDK', err)
|
||||
}
|
||||
|
||||
try {
|
||||
const { getPostHogClient } = await import('@/lib/posthog/server')
|
||||
await getPostHogClient()?.shutdown()
|
||||
logger.info('PostHog client shut down successfully')
|
||||
} catch (err) {
|
||||
logger.error('Error shutting down PostHog client', err)
|
||||
}
|
||||
}
|
||||
|
||||
process.on('SIGTERM', shutdownHandler)
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user