.agents/skills/piece-builder/props-patterns.md
Used in both createAction({ props: {...} }) and createTrigger({ props: {...} }).
Property.ShortText({
displayName: 'Title',
description: 'Optional help text',
required: true,
defaultValue: 'Default text',
})
Property.LongText({
displayName: 'Body',
required: false,
})
Property.Number({
displayName: 'Limit',
required: false,
defaultValue: 10,
})
Property.Checkbox({
displayName: 'Include archived?',
required: false,
defaultValue: false,
})
Property.DateTime({
displayName: 'Due Date',
required: false,
})
Property.File({
displayName: 'Attachment',
required: false,
})
Property.Json({
displayName: 'Custom Data',
description: 'Enter valid JSON',
required: false,
})
Property.Object({
displayName: 'Metadata',
description: 'Key-value pairs',
required: false,
})
// Simple array of strings
Property.Array({
displayName: 'Tags',
required: false,
})
// Array with structured sub-properties
Property.Array({
displayName: 'Line Items',
required: true,
properties: {
name: Property.ShortText({ displayName: 'Item Name', required: true }),
quantity: Property.Number({ displayName: 'Quantity', required: true }),
price: Property.Number({ displayName: 'Price', required: false }),
},
})
Property.StaticDropdown({
displayName: 'Status',
required: true,
options: {
options: [
{ label: 'Active', value: 'active' },
{ label: 'Inactive', value: 'inactive' },
{ label: 'Archived', value: 'archived' },
],
},
})
Property.StaticMultiSelectDropdown({
displayName: 'Categories',
required: false,
options: {
options: [
{ label: 'Sales', value: 'sales' },
{ label: 'Marketing', value: 'marketing' },
],
},
})
Property.Dropdown({
displayName: 'Project',
auth:myAppAuth,
refreshers: [], // Array of prop names this depends on
required: true,
options: async ({ auth }) => {
if (!auth) {
return { disabled: true, options: [], placeholder: 'Please connect your account first' };
}
const response = await httpClient.sendRequest<{ data: { id: string; name: string }[] }>({
method: HttpMethod.GET,
url: 'https://api.example.com/v1/projects',
authentication: { type: AuthenticationType.BEARER_TOKEN, token: auth as string },
});
return {
disabled: false,
options: response.body.data.map((item) => ({
label: item.name,
value: item.id,
})),
};
},
})
Property.Dropdown({
displayName: 'Task',
refreshers: ['project'], // Re-fetches when 'project' prop changes
required: true,
auth:myAppAuth,
options: async ({ auth, project }) => {
if (!auth || !project) {
return { disabled: true, options: [], placeholder: 'Please select a project first' };
}
const response = await httpClient.sendRequest<{ data: { id: string; name: string }[] }>({
method: HttpMethod.GET,
url: `https://api.example.com/v1/projects/${project}/tasks`,
authentication: { type: AuthenticationType.BEARER_TOKEN, token: auth as string },
});
return {
disabled: false,
options: response.body.data.map((item) => ({ label: item.name, value: item.id })),
};
},
})
Real example: packages/pieces/community/github/src/lib/common/index.ts -- see repositoryDropdown, issueDropdown, labelDropDown
Property.MultiSelectDropdown({
displayName: 'Labels',
refreshers: ['repository'],
required: false,
auth:myAppAuth,
options: async ({ auth, repository }) => {
// Same pattern as Dropdown, returns multiple selected values
},
})
For forms where the fields themselves come from the API (e.g., custom table columns):
Property.DynamicProperties({
displayName: 'Record Fields',
refreshers: ['tableId'],
required: true,
auth: myAppAuth,
props: async ({ auth, tableId }): Promise<DynamicPropsValue> => {
if (!auth || !tableId) return {};
const fields = await fetchTableFields(auth, tableId);
const properties: DynamicPropsValue = {};
for (const field of fields) {
properties[field.id] = Property.ShortText({
displayName: field.name,
required: field.required,
});
}
return properties;
},
})
When a user must choose between two mutually exclusive input methods (upload vs S3, URL vs file, etc.), use a StaticDropdown selector refresher + DynamicProperties:
source: Property.StaticDropdown({
displayName: 'File Source',
description: 'Choose how to provide the file.',
required: true,
defaultValue: 'file',
options: {
options: [
{ label: 'Upload a file', value: 'file' },
{ label: 'From S3 bucket', value: 's3' },
],
},
}),
document: Property.DynamicProperties({
auth: myAppAuth,
displayName: 'File',
required: true,
refreshers: ['source'],
// IMPORTANT: explicit Promise<DynamicPropsValue> return type is required
// for the UI to re-render when the source selector changes.
props: async ({ source }): Promise<DynamicPropsValue> => {
if (source === 's3') {
return {
s3Bucket: Property.ShortText({
displayName: 'S3 Bucket',
required: true,
}),
s3Key: Property.ShortText({
displayName: 'S3 File Path',
required: true,
}),
};
}
return {
file: Property.File({
displayName: 'File',
required: true,
}),
};
},
}),
Rules:
Promise<DynamicPropsValue> as the explicit return type — without it the UI will not react to selector changesreturn {} after all branchessource === 's3') — no as unknown as string cast neededdefaultValue on the selector so the first branch renders on loadReading values in run:
const file = source === 'file' ? document['file'] : undefined;
const s3Bucket = source === 's3' ? (document['s3Bucket'] as string) : undefined;
const s3Key = source === 's3' ? (document['s3Key'] as string) : undefined;
Property.MarkDown({
value: '## Instructions\n1. Go to Settings\n2. Copy your API key',
})
Use for setup instructions, warnings, or webhook URL display. See ux-guidelines.md for when to use it.
Use in createPiece({ categories: [...] }):
ARTIFICIAL_INTELLIGENCE, BUSINESS_INTELLIGENCE, COMMUNICATION, COMMERCE, ACCOUNTING, CONTENT_AND_FILES, CUSTOMER_SUPPORT, DEVELOPER_TOOLS, FORMS_AND_SURVEYS, HUMAN_RESOURCES, MARKETING, PAYMENT_PROCESSING, PRODUCTIVITY, SALES_AND_CRM, CORE, FLOW_CONTROL, UNIVERSAL_AI