.agents/skills/piece-builder/common-patterns.md
Always use httpClient from @activepieces/pieces-common:
import { httpClient, HttpMethod, AuthenticationType } from '@activepieces/pieces-common';
const response = await httpClient.sendRequest<{ data: Item[] }>({
method: HttpMethod.GET,
url: 'https://api.example.com/v1/records',
authentication: {
type: AuthenticationType.BEARER_TOKEN,
token: apiKey,
},
queryParams: { limit: '100', page: '1' },
});
// response.body, response.status, response.headers
const response = await httpClient.sendRequest({
method: HttpMethod.POST,
url: 'https://api.example.com/v1/records',
authentication: {
type: AuthenticationType.BEARER_TOKEN,
token: apiKey,
},
body: { name: 'New Record', status: 'active' },
});
const response = await httpClient.sendRequest({
method: HttpMethod.GET,
url: 'https://api.example.com/v1/records',
authentication: {
type: AuthenticationType.BASIC,
username: 'user',
password: 'pass',
},
});
const response = await httpClient.sendRequest({
method: HttpMethod.GET,
url: 'https://api.example.com/v1/records',
headers: {
'Authorization': `Bearer ${apiKey}`,
'X-Custom-Header': 'value',
},
});
GET, POST, PUT, PATCH, DELETE, HEAD, OPTIONS
BEARER_TOKEN, BASIC
For pieces with many actions sharing API logic, create src/lib/common/index.ts:
import {
httpClient,
HttpMethod,
AuthenticationType,
HttpMessageBody,
HttpResponse,
} from '@activepieces/pieces-common';
import { Property } from '@activepieces/pieces-framework';
import { myAppAuth } from '../..';
const BASE_URL = 'https://api.example.com/v1';
// Centralized API call function
export async function myAppApiCall<T extends HttpMessageBody>({
token,
method,
path,
body,
queryParams,
}: {
token: string;
method: HttpMethod;
path: string;
body?: unknown;
queryParams?: Record<string, string>;
}): Promise<HttpResponse<T>> {
return await httpClient.sendRequest<T>({
method,
url: `${BASE_URL}${path}`,
authentication: {
type: AuthenticationType.BEARER_TOKEN,
token,
},
queryParams,
body,
});
}
// Reusable dropdown definitions
export const myAppCommon = {
projectDropdown: Property.Dropdown({
displayName: 'Project',
refreshers: [],
required: true,
options: async ({ auth }) => {
if (!auth) {
return { disabled: true, options: [], placeholder: 'Connect your account first' };
}
const response = await myAppApiCall<{ data: { id: string; name: string }[] }>({
token: auth as string,
method: HttpMethod.GET,
path: '/projects',
});
return {
disabled: false,
options: response.body.data.map((p) => ({
label: p.name,
value: p.id,
})),
};
},
}),
};
Then use in actions:
import { myAppCommon, myAppApiCall } from '../common';
import { myAppAuth } from '../../';
export const listTasksAction = createAction({
auth: myAppAuth,
name: 'list_tasks',
displayName: 'List Tasks',
description: 'Lists tasks in a project',
props: {
project: myAppCommon.projectDropdown, // Reuse the dropdown
},
async run(context) {
const response = await myAppApiCall<{ data: any[] }>({
token: context.auth as string,
method: HttpMethod.GET,
path: `/projects/${context.propsValue.project}/tasks`,
});
return response.body;
},
});
Real examples:
packages/pieces/community/github/src/lib/common/index.tspackages/pieces/community/stripe/src/lib/common/index.tsFor APIs returning paginated results:
export async function myAppPaginatedApiCall<T>({
token, method, path, queryParams,
}: {
token: string;
method: HttpMethod;
path: string;
queryParams?: Record<string, string | number>;
}): Promise<T[]> {
const results: T[] = [];
let page = 1;
const perPage = 100;
let hasMore = true;
while (hasMore) {
const response = await myAppApiCall<{ data: T[]; has_more: boolean }>({
token,
method,
path,
queryParams: {
...queryParams,
page: String(page),
per_page: String(perPage),
} as Record<string, string>,
});
results.push(...response.body.data);
hasMore = response.body.has_more;
page++;
}
return results;
}
Real example: packages/pieces/community/github/src/lib/common/index.ts -- githubPaginatedApiCall
Always add this to give power users a generic HTTP action:
import { createCustomApiCallAction } from '@activepieces/pieces-common';
// In createPiece actions array:
createCustomApiCallAction({
baseUrl: () => 'https://api.example.com/v1',
auth: myAppAuth,
authMapping: async (auth) => ({
Authorization: `Bearer ${auth}`,
}),
})
For OAuth2 auth:
import { OAuth2PropertyValue } from '@activepieces/pieces-framework';
createCustomApiCallAction({
baseUrl: () => 'https://api.example.com',
auth: myAppAuth,
authMapping: async (auth) => ({
Authorization: `Bearer ${(auth as OAuth2PropertyValue).access_token}`,
}),
})
Errors thrown in run() are shown to the user automatically. You can add context:
async run(context) {
try {
const response = await httpClient.sendRequest({ /* ... */ });
return response.body;
} catch (error) {
throw new Error(`Failed to create record: ${(error as Error).message}`);
}
}
Return a disabled state with a message:
options: async ({ auth }) => {
if (!auth) {
return { disabled: true, options: [], placeholder: 'Connect your account first' };
}
try {
const response = await httpClient.sendRequest({ /* ... */ });
return { disabled: false, options: [...] };
} catch (error) {
return { disabled: true, options: [], placeholder: 'Failed to load options. Check connection.' };
}
}