Add Activepieces integration for workflow automation

- Add Activepieces fork with SmoothSchedule custom piece
- Create integrations app with Activepieces service layer
- Add embed token endpoint for iframe integration
- Create Automations page with embedded workflow builder
- Add sidebar visibility fix for embed mode
- Add list inactive customers endpoint to Public API
- Include SmoothSchedule triggers: event created/updated/cancelled
- Include SmoothSchedule actions: create/update/cancel events, list resources/services/customers

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
poduck
2025-12-18 22:59:37 -05:00
parent 9848268d34
commit 3aa7199503
16292 changed files with 1284892 additions and 4708 deletions

View File

@@ -0,0 +1,43 @@
import { createAction, Property } from '@activepieces/pieces-framework';
import { AgentXAuth } from '../common/auth';
import { AgentIdDropdown } from '../common/dropdown';
import { makeRequest } from '../common/client';
import { HttpMethod } from '@activepieces/pieces-common';
export const createConversationWithSingleAgent = createAction({
auth: AgentXAuth,
name: 'createConversationWithSingleAgent',
displayName: 'Create Conversation With Single Agent',
description: 'Create a new conversation with a specific Agent by ID.',
props: {
agentId: AgentIdDropdown,
type: Property.StaticDropdown({
displayName: "Conversation Type",
description: "Choose the type of conversation",
required: true,
options: {
disabled: false,
options: [
{ label: "Chat", value: "chat" },
{ label: "Search", value: "search" },
{ label: "Ecommerce", value: "ecommerce" },
],
},
}),
},
async run({ auth, propsValue }) {
const { agentId, type } = propsValue;
const response = await makeRequest(
auth.secret_text,
HttpMethod.POST,
`/agents/${agentId}/conversations/new`,
{
type
}
)
return response;
},
});

View File

@@ -0,0 +1,66 @@
import { createAction, Property } from "@activepieces/pieces-framework";
import { makeRequest } from "../common/client";
import { AgentXAuth } from "../common/auth";
import { HttpMethod } from "@activepieces/pieces-common";
import { AgentIdDropdown, ConversationIdDropdown } from "../common/dropdown";
export const findConversation = createAction({
auth: AgentXAuth,
name: "find_conversation",
displayName: "Find Conversation",
description:
"Looks up an existing conversation by Agent ID (optionally by conversation ID or Name).",
props: {
agentId: AgentIdDropdown,
conversationId: Property.ShortText({
displayName: "Conversation ID",
description: "Search by exact Conversation ID.",
required: false,
}),
conversationName: Property.ShortText({
displayName: "Conversation Name,tittle or ID",
description:
"Search conversation by its name (derived from the first message).",
required: false,
}),
},
async run({ auth, propsValue }) {
const { agentId, conversationId, conversationName } = propsValue;
const addName = (conversation: any) => ({
...conversation,
name:
conversation.messages?.[0]?.text?.slice(0, 30) ||
`Conversation ${conversation._id || conversation.id}`,
});
const conversations = await makeRequest(
auth.secret_text,
HttpMethod.GET,
`/agents/${agentId}/conversations`
);
if (!Array.isArray(conversations)) {
throw new Error(
"Unexpected response from AgentX API: expected an array of conversations."
);
}
const conversationsWithName = conversations.map(addName);
if (conversationName) {
return conversationsWithName.filter((c) =>
c.name.toLowerCase().includes(conversationName.toLowerCase())
);
}
if (conversationId) {
return conversationsWithName.filter(
(c) => c.id === conversationId || c._id === conversationId
);
}
return conversationsWithName;
},
});

View File

@@ -0,0 +1,59 @@
import { createAction, Property } from "@activepieces/pieces-framework";
import { HttpMethod } from "@activepieces/pieces-common";
import { makeRequest } from "../common/client";
import { AgentXAuth } from "../common/auth";
import { AgentIdDropdown, ConversationIdDropdown } from "../common/dropdown";
type Message = {
id: string;
text?: string;
[key: string]: unknown;
};
type ConversationDetail = {
id: string;
messages: Message[];
[key: string]: unknown;
};
export const findMessage = createAction({
auth: AgentXAuth,
name: "find_message",
displayName: "Find Message",
description:
"Searches for a specific message by ID (or searches text within messages) inside a conversation.",
props: {
agentId: AgentIdDropdown,
conversationId: ConversationIdDropdown,
searchTerm: Property.ShortText({
displayName: "Search Term",
description:
"Text to search for in message content. Leave blank if searching by Message ID.",
required: false,
}),
},
async run({ auth, propsValue }) {
const { agentId, conversationId, searchTerm } = propsValue;
const conversation = await makeRequest(
auth.secret_text,
HttpMethod.GET,
`/agents/${agentId}/conversations/${conversationId}`
) as ConversationDetail;
const allMessages = conversation?.messages || [];
if (!searchTerm) {
return allMessages;
}
const lowerCaseSearchTerm = searchTerm.toLowerCase();
return allMessages.filter(
(m: any) =>
(m.text && m.text.toLowerCase().includes(lowerCaseSearchTerm)) ||
(m.content && m.content.toLowerCase().includes(lowerCaseSearchTerm))
);
},
});

View File

@@ -0,0 +1,49 @@
import { createAction, Property } from "@activepieces/pieces-framework";
import { makeRequest } from "../common/client";
import { AgentXAuth } from "../common/auth";
import { HttpMethod } from "@activepieces/pieces-common";
export const searchAgents = createAction({
auth: AgentXAuth,
name: "search_agents",
displayName: "Search Agents",
description: "Find agents by name or ID using search filters.",
props: {
name: Property.ShortText({
displayName: "Agent Name",
description: "Search by agent name (partial matches allowed).",
required: false,
}),
id: Property.ShortText({
displayName: "Agent ID",
description: "Search by exact Agent ID.",
required: false,
}),
},
async run({ auth, propsValue }) {
const { name, id } = propsValue;
const agents = await makeRequest(auth.secret_text, HttpMethod.GET, "/agents");
if (!Array.isArray(agents)) {
throw new Error("Unexpected response from AgentX API: expected an array of agents.");
}
const filtered = agents.filter((agent: any) => {
let matches = true;
if (id) {
matches = matches && agent._id === id;
}
if (name && agent.name) {
matches = matches && agent.name.toLowerCase().includes(name.toLowerCase());
}
return matches;
});
return filtered;
},
});

View File

@@ -0,0 +1,56 @@
import { createAction, Property } from '@activepieces/pieces-framework';
import { AgentXAuth } from '../common/auth';
import { makeRequest } from '../common/client';
import { HttpMethod } from '@activepieces/pieces-common';
import { ConversationIdDropdown } from '../common/dropdown';
import { AgentIdDropdown } from '../common/dropdown';
export const sendMessageToExistingConversation = createAction({
auth: AgentXAuth,
name: 'sendMessageToExistingConversation',
displayName: 'Send Message to Existing Conversation',
description: 'Send a message to an existing conversation with an agent.',
props: {
agentId: AgentIdDropdown,
conversationId: ConversationIdDropdown,
agentMode: Property.StaticDropdown({
displayName: "Agent Mode",
description: "Choose how the agent should respond",
required: true,
options: {
disabled: false,
options: [
{ label: "Chat", value: "chat" },
{ label: "Search", value: "search" },
],
},
}),
message: Property.LongText({
displayName: "Message",
description: "The message you want to send to the agent.",
required: true,
}),
context: Property.Number({
displayName: "Memory Context",
description:
"Number of previous messages to include. 0 = as much as possible, 1 = only last message, etc.",
required: false,
}),
},
async run({ auth, propsValue }) {
const { conversationId, agentMode, message, context } = propsValue;
const response = await makeRequest(
auth.secret_text,
HttpMethod.POST,
`/conversations/${conversationId}/message`,
{
conversationId,
agentMode,
message,
context,
}
)
return response;
},
});

View File

@@ -0,0 +1,36 @@
import { PieceAuth } from "@activepieces/pieces-framework";
import { makeRequest } from "./client";
import { HttpMethod } from "@activepieces/pieces-common";
export const AgentXAuth = PieceAuth.SecretText({
displayName: 'AgentX API Key',
description: `**Enter your AgentX API Key**
---
### How to obtain your API key
1. Visit [AgentX](https://www.agentx.so/) and log in.
2. Click on your avatar (bottom-left corner).
3. Copy your API Key from the popup window.
4. Paste it here.
`,
required: true,
validate: async ({ auth }) => {
if (auth) {
try {
await makeRequest(auth as string, HttpMethod.GET, "/agents");
return {
valid: true,
};
} catch (error) {
return {
valid: false,
error: "Invalid API Key or authentication failed.",
};
}
}
return {
valid: false,
error: "API Key is required.",
};
},
});

View File

@@ -0,0 +1,26 @@
import { HttpMethod, httpClient } from "@activepieces/pieces-common";
export const BASE_URL = `https://api.agentx.so/api/v1/access`;
export async function makeRequest(
apiKey: string,
method: HttpMethod,
path: string,
body?: unknown
) {
try {
const response = await httpClient.sendRequest({
method,
url: `${BASE_URL}${path}`,
headers: {
"x-api-key": apiKey,
"Content-Type": "application/json",
Accept: "application/json",
},
body,
});
return response.body;
} catch (error: any) {
throw new Error(`AgentX API error: ${error.message || String(error)}`);
}
}

View File

@@ -0,0 +1,86 @@
import { Property } from "@activepieces/pieces-framework";
import { makeRequest } from "./client";
import { HttpMethod } from "@activepieces/pieces-common";
import { AgentXAuth } from "./auth";
export const AgentIdDropdown = Property.Dropdown<string,true,typeof AgentXAuth>({
auth: AgentXAuth,
displayName: "Agent",
description: "Select an AgentX agent",
required: true, // ensures the value is always a string, not undefined
refreshers: ["auth"],
options: async ({ auth }) => {
if (!auth) {
return {
disabled: true,
placeholder: "Please connect your AgentX account first",
options: [],
};
}
try {
const agents = await makeRequest(auth.secret_text, HttpMethod.GET, "/agents");
return {
disabled: false,
options: agents.map((agent: any) => ({
label: agent.name || `Agent ${agent.id}`,
value: agent._id, // must be string
})),
};
} catch (error: any) {
return {
disabled: true,
placeholder: `Failed to fetch agents: ${error.message || error}`,
options: [],
};
}
},
});
export const ConversationIdDropdown = Property.Dropdown({
auth: AgentXAuth,
displayName: "Conversation",
description: "Select a conversation for the chosen Agent",
required: false,
refreshers: ["auth", "agentId"],
options: async ({ auth, agentId }) => {
if (!auth) {
return {
disabled: true,
placeholder: "Please connect your AgentX account first",
options: [],
};
}
if (!agentId) {
return {
disabled: true,
placeholder: "Select an Agent first",
options: [],
};
}
try {
const conversations = await makeRequest(
auth.secret_text,
HttpMethod.GET,
`/agents/${agentId}/conversations`
);
return {
disabled: false,
options: conversations.map((c: any) => ({
label: c.title || `Conversation ${c._id}`,
value: c._id,
})),
};
} catch (error: any) {
return {
disabled: true,
placeholder: `Failed to fetch conversations: ${error.message || error}`,
options: [],
};
}
},
});

View File

@@ -0,0 +1,63 @@
import {createTrigger,TriggerStrategy, AppConnectionValueForAuthProperty,} from "@activepieces/pieces-framework";
import {DedupeStrategy,Polling,pollingHelper,} from "@activepieces/pieces-common";
import dayjs from "dayjs";
import { makeRequest } from "../common/client";
import { HttpMethod } from "@activepieces/pieces-common";
import { AgentXAuth } from "../common/auth";
type Agent = {
id: string;
name?: string;
createdAt: string;
};
const polling: Polling<
AppConnectionValueForAuthProperty<typeof AgentXAuth>,
Record<string, never>
> = {
strategy: DedupeStrategy.TIMEBASED,
items: async ({ auth }) => {
const agents = (await makeRequest(auth.secret_text, HttpMethod.GET, "/agents")) as Agent[];
const sortedAgents = agents.sort((a, b) =>
dayjs(b.createdAt).valueOf() - dayjs(a.createdAt).valueOf()
);
return sortedAgents.map((agent) => ({
epochMilliSeconds: dayjs(agent.createdAt).valueOf(),
data: agent,
}));
},
};
export const newAgent = createTrigger({
auth: AgentXAuth,
name: "new_agent",
displayName: "New Agent",
description: "Triggers when a new AgentX agent is created.",
props: {},
sampleData: {
_id: "agt_1234567890abcdef",
name: "Customer Support Bot",
created_at: "2025-09-08T10:00:00Z",
},
type: TriggerStrategy.POLLING,
async test(context) {
return await pollingHelper.test(polling, context);
},
async onEnable(context) {
const { store, auth, propsValue } = context;
await pollingHelper.onEnable(polling, { store, auth, propsValue });
},
async onDisable(context) {
const { store, auth, propsValue } = context;
await pollingHelper.onDisable(polling, { store, auth, propsValue });
},
async run(context) {
return await pollingHelper.poll(polling, context);
},
});

View File

@@ -0,0 +1,90 @@
import {
createTrigger,
TriggerStrategy,
AppConnectionValueForAuthProperty,
} from "@activepieces/pieces-framework";
import {
DedupeStrategy,
Polling,
pollingHelper,
HttpMethod,
} from "@activepieces/pieces-common";
import dayjs from "dayjs";
import { makeRequest } from "../common/client";
import { AgentXAuth } from "../common/auth";
import { AgentIdDropdown } from "../common/dropdown";
type Conversation = {
_id: string;
type?: string;
createdAt: string;
};
const polling: Polling<
AppConnectionValueForAuthProperty<typeof AgentXAuth>,
{ agentId?: string }
> = {
strategy: DedupeStrategy.TIMEBASED,
items: async ({ auth, propsValue }) => {
const { agentId } = propsValue;
if (!agentId) {
return [];
}
try {
const conversations = (await makeRequest(
auth.secret_text,
HttpMethod.GET,
`/agents/${agentId}/conversations`
)) as Conversation[];
const sortedConversations = conversations.sort((a, b) =>
dayjs(b.createdAt).valueOf() - dayjs(a.createdAt).valueOf()
);
return sortedConversations.map((conv) => ({
epochMilliSeconds: dayjs(conv.createdAt).valueOf(),
data: conv,
}));
} catch (error) {
console.error(`Error fetching conversations for agent ${agentId}:`, error);
throw error;
}
},
};
export const newConversation = createTrigger({
auth: AgentXAuth,
name: "new_conversation",
displayName: "New Conversation",
description: "Triggers when a new conversation begins with a specific Agent. Only detects conversations created after the trigger is enabled.",
type: TriggerStrategy.POLLING,
props: {
agentId: AgentIdDropdown,
},
sampleData: {
id: "conv_1234567890abcdef",
type: "chat",
created_at: "2025-09-08T11:45:00Z",
},
async test(context) {
return await pollingHelper.test(polling, context);
},
async onEnable(context) {
await pollingHelper.onEnable(polling, context);
},
async onDisable(context) {
await pollingHelper.onDisable(polling, context);
},
async run(context) {
return await pollingHelper.poll(polling, context);
},
});