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,30 @@
import { createPiece } from "@activepieces/pieces-framework";
import { PieceCategory } from "@activepieces/shared";
import { lettaAuth } from "./lib/common/auth";
import { createAgentFromTemplate } from "./lib/actions/create-agent-from-template";
import { createIdentity } from "./lib/actions/create-identity";
import { sendMessageToAgent } from "./lib/actions/send-message-to-agent";
import { getIdentities } from "./lib/actions/get-identities";
import { newAgent } from "./lib/triggers/new-agent";
import { newMessage } from "./lib/triggers/new-message";
export const letta = createPiece({
displayName: "Letta",
description: "Letta is the platform for building stateful agents: open AI with advanced memory that can learn and self-improve over time.",
auth: lettaAuth,
minimumSupportedRelease: '0.36.1',
logoUrl: "https://cdn.activepieces.com/pieces/letta.png",
categories: [PieceCategory.ARTIFICIAL_INTELLIGENCE],
authors: ["onyedikachi-david"],
actions: [
createAgentFromTemplate,
createIdentity,
sendMessageToAgent,
getIdentities,
],
triggers: [
newAgent,
newMessage,
],
});

View File

@@ -0,0 +1,144 @@
import { createAction, Property } from '@activepieces/pieces-framework';
import { lettaAuth } from '../common/auth';
import { getLettaClient } from '../common/client';
import { identityIdsDropdown } from '../common/props';
import type {
AgentCreateParams,
AgentCreateResponse,
} from '../common/types';
export const createAgentFromTemplate = createAction({
auth: lettaAuth,
name: 'createAgentFromTemplate',
displayName: 'Create Agent From Template',
description: 'Creates an agent from a template',
props: {
templateVersion: Property.ShortText({
displayName: 'Template Version',
description: 'The template version ID to create the agent from',
required: true,
}),
agentName: Property.ShortText({
displayName: 'Agent Name',
description: 'The name of the agent (optional, a random name will be assigned if not provided)',
required: false,
}),
identityIds: identityIdsDropdown,
tags: Property.Array({
displayName: 'Tags',
description: 'Tags to assign to the agent',
required: false,
properties: {
tag: Property.ShortText({
displayName: 'Tag',
description: 'Tag name',
required: true,
}),
},
}),
memoryVariables: Property.Object({
displayName: 'Memory Variables',
description: 'Memory variables to assign to the agent (key-value pairs)',
required: false,
}),
toolVariables: Property.Object({
displayName: 'Tool Variables',
description: 'Tool variables to assign to the agent (key-value pairs)',
required: false,
}),
initialMessageSequence: Property.Array({
displayName: 'Initial Message Sequence',
description: 'Initial sequence of messages to start the agent with',
required: false,
properties: {
content: Property.LongText({
displayName: 'Content',
description: 'Message content',
required: true,
}),
role: Property.StaticDropdown({
displayName: 'Role',
description: 'Message role',
required: true,
options: {
disabled: false,
options: [
{ label: 'User', value: 'user' },
{ label: 'System', value: 'system' },
{ label: 'Assistant', value: 'assistant' },
],
},
}),
name: Property.ShortText({
displayName: 'Name',
description: 'Optional name of the participant',
required: false,
}),
},
}),
},
async run(context) {
const {
templateVersion,
agentName,
identityIds,
tags,
memoryVariables,
toolVariables,
initialMessageSequence,
} = context.propsValue;
const client = getLettaClient(context.auth.props);
const body: AgentCreateParams = {};
if (agentName) {
body.agent_name = agentName;
}
if (identityIds && identityIds.length > 0) {
body.identity_ids = identityIds;
}
if (tags && tags.length > 0) {
body.tags = tags.map((tagObj: any) => tagObj.tag).filter(Boolean);
}
if (memoryVariables && Object.keys(memoryVariables).length > 0) {
const memoryVars: Record<string, string> = {};
for (const [key, value] of Object.entries(memoryVariables)) {
memoryVars[key] = String(value);
}
body.memory_variables = memoryVars;
}
if (toolVariables && Object.keys(toolVariables).length > 0) {
const toolVars: Record<string, string> = {};
for (const [key, value] of Object.entries(toolVariables)) {
toolVars[key] = String(value);
}
body.tool_variables = toolVars;
}
if (initialMessageSequence && initialMessageSequence.length > 0) {
body.initial_message_sequence = initialMessageSequence.map((msg: any) => ({
content: msg.content,
role: msg.role,
name: msg.name || undefined,
}));
}
const response: AgentCreateResponse = await client.templates.agents.create(
templateVersion,
body
);
return {
agentIds: response.agent_ids,
deploymentId: response.deployment_id,
groupId: response.group_id,
success: true,
};
},
});

View File

@@ -0,0 +1,142 @@
import { createAction, Property } from '@activepieces/pieces-framework';
import { lettaAuth } from '../common/auth';
import { getLettaClient } from '../common/client';
import type {
IdentityCreateParams,
Identity,
IdentityProperty,
} from '../common/types';
export const createIdentity = createAction({
auth: lettaAuth,
name: 'createIdentity',
displayName: 'Create Identity',
description: 'Creates a Letta identity',
props: {
identifierKey: Property.ShortText({
displayName: 'Identifier Key',
description: 'External, user-generated identifier key of the identity',
required: true,
}),
name: Property.ShortText({
displayName: 'Name',
description: 'The name of the identity',
required: true,
}),
identityType: Property.StaticDropdown({
displayName: 'Identity Type',
description: 'The type of the identity',
required: true,
options: {
disabled: false,
options: [
{ label: 'Organization', value: 'org' },
{ label: 'User', value: 'user' },
{ label: 'Other', value: 'other' },
],
},
}),
projectId: Property.ShortText({
displayName: 'Project ID',
description: 'The project ID of the identity (optional)',
required: false,
}),
properties: Property.Array({
displayName: 'Properties',
description: 'List of properties associated with the identity',
required: false,
properties: {
key: Property.ShortText({
displayName: 'Key',
description: 'Property key',
required: true,
}),
type: Property.StaticDropdown({
displayName: 'Type',
description: 'Property type',
required: true,
options: {
disabled: false,
options: [
{ label: 'String', value: 'string' },
{ label: 'Number', value: 'number' },
{ label: 'Boolean', value: 'boolean' },
{ label: 'JSON', value: 'json' },
],
},
}),
value: Property.ShortText({
displayName: 'Value',
description: 'Property value (for JSON type, enter valid JSON string)',
required: true,
}),
},
}),
},
async run(context) {
const {
identifierKey,
name,
identityType,
projectId,
properties,
} = context.propsValue;
const client = getLettaClient(context.auth.props);
const body: IdentityCreateParams = {
identifier_key: identifierKey,
name: name,
identity_type: identityType as 'org' | 'user' | 'other',
};
if (projectId) {
body.project_id = projectId;
}
if (properties && properties.length > 0) {
body.properties = properties.map((prop: any) => {
let parsedValue: string | number | boolean | { [key: string]: unknown };
switch (prop.type) {
case 'number':
parsedValue = Number(prop.value);
break;
case 'boolean':
parsedValue = prop.value === 'true' || prop.value === true;
break;
case 'json':
try {
parsedValue = JSON.parse(prop.value);
} catch (e) {
throw new Error(`Invalid JSON in property "${prop.key}": ${e instanceof Error ? e.message : 'Unknown error'}`);
}
break;
default:
parsedValue = String(prop.value);
}
const identityProperty: IdentityProperty = {
key: prop.key,
type: prop.type as 'string' | 'number' | 'boolean' | 'json',
value: parsedValue,
};
return identityProperty;
});
}
const response: Identity = await client.identities.create(body);
return {
id: response.id,
identifierKey: response.identifier_key,
name: response.name,
identityType: response.identity_type,
projectId: response.project_id,
properties: response.properties,
success: true,
};
},
});

View File

@@ -0,0 +1,97 @@
import { createAction, Property } from '@activepieces/pieces-framework';
import { lettaAuth } from '../common/auth';
import { getLettaClient } from '../common/client';
import type {
IdentityListParams,
Identity,
} from '../common/types';
export const getIdentities = createAction({
auth: lettaAuth,
name: 'getIdentities',
displayName: 'Get Identities',
description: 'Searches for identities in your Letta Project',
props: {
identifierKey: Property.ShortText({
displayName: 'Identifier Key',
description: 'Filter by identifier key',
required: false,
}),
name: Property.ShortText({
displayName: 'Name',
description: 'Filter by identity name',
required: false,
}),
identityType: Property.StaticDropdown({
displayName: 'Identity Type',
description: 'Filter by identity type',
required: false,
options: {
disabled: false,
options: [
{ label: 'Organization', value: 'org' },
{ label: 'User', value: 'user' },
{ label: 'Other', value: 'other' },
],
},
}),
projectId: Property.ShortText({
displayName: 'Project ID',
description: 'Filter by project ID',
required: false,
}),
limit: Property.Number({
displayName: 'Limit',
description: 'Maximum number of identities to return',
required: false,
defaultValue: 100,
}),
},
async run(context) {
const {
identifierKey,
name,
identityType,
projectId,
limit,
} = context.propsValue;
const client = getLettaClient(context.auth.props);
const query: IdentityListParams = {};
if (identifierKey) {
query.identifier_key = identifierKey;
}
if (name) {
query.name = name;
}
if (identityType) {
query.identity_type = identityType as 'org' | 'user' | 'other';
}
if (projectId) {
query.project_id = projectId;
}
if (limit !== undefined && limit !== null) {
query.limit = limit;
}
const identitiesPage = await client.identities.list(query);
const identities: Identity[] = [];
for await (const identity of identitiesPage) {
identities.push(identity);
}
return {
identities,
count: identities.length,
success: true,
};
},
});

View File

@@ -0,0 +1,58 @@
import { createAction, Property } from '@activepieces/pieces-framework';
import { lettaAuth } from '../common/auth';
import { getLettaClient } from '../common/client';
import { agentIdDropdown } from '../common/props';
import type {
MessageCreateParamsNonStreaming,
LettaResponse,
} from '../common/types';
export const sendMessageToAgent = createAction({
auth: lettaAuth,
name: 'sendMessageToAgent',
displayName: 'Send Message to Agent',
description: 'Send message to an agent',
props: {
agentId: agentIdDropdown,
input: Property.LongText({
displayName: 'Message',
description: 'The message content to send to the agent',
required: true,
}),
maxSteps: Property.Number({
displayName: 'Max Steps',
description: 'Maximum number of steps the agent should take to process the request',
required: false,
}),
},
async run(context) {
const {
agentId,
input,
maxSteps,
} = context.propsValue;
const client = getLettaClient(context.auth.props);
const body: MessageCreateParamsNonStreaming = {
streaming: false,
input: input,
};
if (maxSteps !== undefined && maxSteps !== null) {
body.max_steps = maxSteps;
}
const response: LettaResponse = await client.agents.messages.create(
agentId,
body
);
return {
messages: response.messages,
stopReason: response.stop_reason,
success: true,
};
},
});

View File

@@ -0,0 +1,107 @@
import { PieceAuth, Property } from '@activepieces/pieces-framework';
import {
Letta,
AuthenticationError,
PermissionDeniedError,
APIConnectionError,
APIConnectionTimeoutError,
LettaError,
} from '@letta-ai/letta-client';
import type {
ClientOptions,
AgentListParams,
} from './types';
const markdown = `
To authenticate with Letta:
1. **For Letta Cloud**: Get your API key from [Letta Cloud](https://cloud.letta.ai)
2. **For Self-hosted**: Leave API key empty and provide your server URL (e.g., http://localhost:8283)
`;
export const lettaAuth = PieceAuth.CustomAuth({
description: markdown,
required: true,
props: {
apiKey: PieceAuth.SecretText({
displayName: 'API Key',
description: 'Your Letta API key (required for Letta Cloud, leave empty for self-hosted)',
required: false,
}),
baseUrl: Property.ShortText({
displayName: 'Base URL',
description: 'Server URL for self-hosted instances (e.g., http://localhost:8283). Leave empty to use Letta Cloud.',
required: false,
}),
},
validate: async ({ auth }) => {
const { apiKey, baseUrl } = auth;
if (!apiKey && !baseUrl) {
return {
valid: false,
error: 'Please provide either an API key (for Letta Cloud) or a base URL (for self-hosted).',
};
}
try {
const clientConfig: ClientOptions = {};
if (apiKey) {
clientConfig.apiKey = apiKey;
}
if (baseUrl) {
clientConfig.baseURL = baseUrl;
}
const client = new Letta(clientConfig);
if (apiKey) {
const listParams: AgentListParams = { limit: 1 };
await client.agents.list(listParams);
} else {
await client.health();
}
return {
valid: true,
};
} catch (error: unknown) {
if (error instanceof AuthenticationError) {
return {
valid: false,
error: 'Invalid API key. Please check your credentials.',
};
}
if (error instanceof PermissionDeniedError) {
return {
valid: false,
error: 'Permission denied. Please check your API key permissions.',
};
}
if (error instanceof APIConnectionError || error instanceof APIConnectionTimeoutError) {
return {
valid: false,
error: 'Connection failed. Please check your base URL and ensure the server is running.',
};
}
if (error instanceof LettaError) {
return {
valid: false,
error: `Authentication failed: ${error instanceof Error ? error.message : 'Unknown error'}. Please verify your credentials.`,
};
}
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
return {
valid: false,
error: `Authentication failed: ${errorMessage}. Please verify your credentials.`,
};
}
},
});
export type LettaAuthType = {
apiKey?: string;
baseUrl?: string;
};

View File

@@ -0,0 +1,19 @@
import { Letta } from '@letta-ai/letta-client';
import type { ClientOptions } from './types';
import { LettaAuthType } from './auth';
export function getLettaClient(auth: LettaAuthType): Letta {
const clientConfig: ClientOptions = {};
if (auth.apiKey) {
clientConfig.apiKey = auth.apiKey;
}
if (auth.baseUrl) {
clientConfig.baseURL = auth.baseUrl;
}
return new Letta(clientConfig);
}

View File

@@ -0,0 +1,103 @@
import { Property } from '@activepieces/pieces-framework';
import { getLettaClient } from './client';
import { lettaAuth, type LettaAuthType } from './auth';
export const identityIdsDropdown = Property.MultiSelectDropdown({
displayName: 'Identities',
description: 'Select identities to assign to the agent',
auth: lettaAuth,
required: false,
refreshers: [],
options: async ({ auth }) => {
if (!auth) {
return {
disabled: true,
options: [],
placeholder: 'Please connect your account first',
};
}
try {
const client = getLettaClient(auth as LettaAuthType);
const identitiesPage = await client.identities.list();
const identities: Array<{ label: string; value: string }> = [];
for await (const identity of identitiesPage) {
identities.push({
label: identity.name || identity.identifier_key || identity.id,
value: identity.id,
});
}
if (identities.length === 0) {
return {
disabled: false,
options: [],
placeholder: 'No identities found',
};
}
return {
disabled: false,
options: identities,
};
} catch (error) {
return {
disabled: true,
placeholder: 'Failed to load identities. Please check your connection.',
options: [],
};
}
},
});
export const agentIdDropdown = Property.Dropdown({
auth: lettaAuth,
displayName: 'Agent',
description: 'Select the agent to send a message to',
required: true,
refreshers: [],
options: async ({ auth }) => {
if (!auth) {
return {
disabled: true,
options: [],
placeholder: 'Please connect your account first',
};
}
try {
const client = getLettaClient(auth as LettaAuthType);
const agentsPage = await client.agents.list();
const agents: Array<{ label: string; value: string }> = [];
for await (const agent of agentsPage) {
agents.push({
label: agent.name || agent.id,
value: agent.id,
});
}
if (agents.length === 0) {
return {
disabled: false,
options: [],
placeholder: 'No agents found',
};
}
return {
disabled: false,
options: agents,
};
} catch (error) {
return {
disabled: true,
placeholder: 'Failed to load agents. Please check your connection.',
options: [],
};
}
},
});

View File

@@ -0,0 +1,27 @@
export type { ClientOptions } from '@letta-ai/letta-client';
export type {
AgentState,
AgentListParams,
} from '@letta-ai/letta-client/resources/agents/agents';
export type {
AgentCreateParams,
AgentCreateResponse,
} from '@letta-ai/letta-client/resources/templates/agents';
export type {
Identity,
IdentityCreateParams,
IdentityListParams,
IdentityProperty,
IdentityType,
} from '@letta-ai/letta-client/resources/identities/identities';
export type {
Message,
MessageCreateParamsNonStreaming,
MessageListParams,
ToolCallMessage,
LettaResponse,
} from '@letta-ai/letta-client/resources/agents/messages';

View File

@@ -0,0 +1,79 @@
import {
DedupeStrategy,
Polling,
pollingHelper,
} from '@activepieces/pieces-common';
import {
TriggerStrategy,
createTrigger,
AppConnectionValueForAuthProperty,
} from '@activepieces/pieces-framework';
import { lettaAuth } from '../common/auth';
import { getLettaClient } from '../common/client';
import type {
AgentState,
AgentListParams,
} from '../common/types';
const polling: Polling<
AppConnectionValueForAuthProperty<typeof lettaAuth>,
Record<string, never>
> = {
strategy: DedupeStrategy.TIMEBASED,
items: async ({ auth, lastFetchEpochMS }) => {
const client = getLettaClient(auth.props);
const query: AgentListParams = {
limit: 100,
};
const agentsPage = await client.agents.list(query);
const agents: AgentState[] = [];
for await (const agent of agentsPage) {
if (agent.created_at) {
const createdAtEpoch = Date.parse(agent.created_at);
if (createdAtEpoch > lastFetchEpochMS) {
agents.push(agent);
}
}
}
return agents
.sort((a, b) => {
const aTime = a.created_at ? Date.parse(a.created_at) : 0;
const bTime = b.created_at ? Date.parse(b.created_at) : 0;
return bTime - aTime;
})
.map((agent) => ({
epochMilliSeconds: agent.created_at
? Date.parse(agent.created_at)
: Date.now(),
data: agent,
}));
},
};
export const newAgent = createTrigger({
auth: lettaAuth,
name: 'newAgent',
displayName: 'New Agent',
description: 'Triggers when a new agent is created',
type: TriggerStrategy.POLLING,
props: {},
sampleData: {},
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);
},
async test(context) {
return await pollingHelper.test(polling, context);
},
});

View File

@@ -0,0 +1,107 @@
import {
DedupeStrategy,
Polling,
pollingHelper,
} from '@activepieces/pieces-common';
import {
TriggerStrategy,
createTrigger,
PiecePropValueSchema,
AppConnectionValueForAuthProperty,
} from '@activepieces/pieces-framework';
import { lettaAuth } from '../common/auth';
import { getLettaClient } from '../common/client';
import { agentIdDropdown } from '../common/props';
import type {
Message,
MessageListParams,
ToolCallMessage,
} from '../common/types';
const polling: Polling<
AppConnectionValueForAuthProperty<typeof lettaAuth>,
{ agentId: string }
> = {
strategy: DedupeStrategy.TIMEBASED,
items: async ({ auth, propsValue, lastFetchEpochMS }) => {
const { agentId } = propsValue;
const client = getLettaClient(auth.props);
const query: MessageListParams = {
limit: 100,
};
const messagesPage = await client.agents.messages.list(agentId, query);
const sendMessageToolCalls: ToolCallMessage[] = [];
for await (const message of messagesPage) {
if (message.message_type === 'tool_call_message') {
const toolCallMessage = message as ToolCallMessage;
let hasSendMessage = false;
if (toolCallMessage.tool_calls) {
if (Array.isArray(toolCallMessage.tool_calls)) {
hasSendMessage = toolCallMessage.tool_calls.some(
(tc) => tc.name === 'send_message'
);
} else {
const delta = toolCallMessage.tool_calls;
hasSendMessage = delta.name === 'send_message';
}
}
if (!hasSendMessage && toolCallMessage.tool_call) {
const toolCall = toolCallMessage.tool_call;
if ('name' in toolCall && toolCall.name) {
hasSendMessage = toolCall.name === 'send_message';
}
}
if (hasSendMessage && toolCallMessage.date) {
const messageDateEpoch = Date.parse(toolCallMessage.date);
if (messageDateEpoch > lastFetchEpochMS) {
sendMessageToolCalls.push(toolCallMessage);
}
}
}
}
return sendMessageToolCalls
.sort((a, b) => {
const aTime = a.date ? Date.parse(a.date) : 0;
const bTime = b.date ? Date.parse(b.date) : 0;
return bTime - aTime;
})
.map((message) => ({
epochMilliSeconds: message.date ? Date.parse(message.date) : Date.now(),
data: message,
}));
},
};
export const newMessage = createTrigger({
auth: lettaAuth,
name: 'newMessage',
displayName: 'New Message',
description: 'Triggers when an agent uses send_message',
type: TriggerStrategy.POLLING,
props: {
agentId: agentIdDropdown,
},
sampleData: {},
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);
},
async test(context) {
return await pollingHelper.test(polling, context);
},
});