From fcb1e87d134480ac5efd01f7c9f36a30bc051a6b Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Thu, 9 Jul 2026 20:41:25 -0700 Subject: [PATCH 1/4] feat(brex): add transfer/budget/spend-limit/vendor write tools - fix get_company transformResponse reading camelCase accountType instead of Brex's account_type field (always null in prod) - add brex_create_transfer, brex_create_budget, brex_archive_budget, brex_create_spend_limit, brex_create_vendor, brex_update_vendor - endpoints verified against Brex's live payments_api.yaml and budgets_api.yaml OpenAPI specs - guard required money-amount fields against blank/NaN input instead of silently coercing to 0 --- apps/sim/blocks/blocks/brex.ts | 488 +++++++++++++++++++++- apps/sim/tools/brex/archive_budget.ts | 71 ++++ apps/sim/tools/brex/create_budget.ts | 145 +++++++ apps/sim/tools/brex/create_spend_limit.ts | 256 ++++++++++++ apps/sim/tools/brex/create_transfer.ts | 189 +++++++++ apps/sim/tools/brex/create_vendor.ts | 75 ++++ apps/sim/tools/brex/index.ts | 6 + apps/sim/tools/brex/types.ts | 159 +++++++ apps/sim/tools/brex/update_vendor.ts | 83 ++++ apps/sim/tools/brex/utils.ts | 13 + apps/sim/tools/registry.ts | 12 + 11 files changed, 1490 insertions(+), 7 deletions(-) create mode 100644 apps/sim/tools/brex/archive_budget.ts create mode 100644 apps/sim/tools/brex/create_budget.ts create mode 100644 apps/sim/tools/brex/create_spend_limit.ts create mode 100644 apps/sim/tools/brex/create_transfer.ts create mode 100644 apps/sim/tools/brex/create_vendor.ts create mode 100644 apps/sim/tools/brex/update_vendor.ts diff --git a/apps/sim/blocks/blocks/brex.ts b/apps/sim/blocks/blocks/brex.ts index 6a5feefbb83..7c7f77223ca 100644 --- a/apps/sim/blocks/blocks/brex.ts +++ b/apps/sim/blocks/blocks/brex.ts @@ -4,6 +4,15 @@ import { AuthMode, IntegrationType } from '@/blocks/types' import { normalizeFileInput } from '@/blocks/utils' import type { BrexResponse } from '@/tools/brex/types' +/** Coerces a required money-amount field to a finite number, throwing on blank/non-numeric input rather than silently sending 0 or NaN to Brex. */ +function toRequiredAmount(value: unknown, fieldLabel: string): number { + const parsed = Number(value) + if (value === '' || value == null || !Number.isFinite(parsed)) { + throw new Error(`${fieldLabel} must be a valid number`) + } + return parsed +} + const PAGINATED_OPERATIONS = new Set([ 'list_expenses', 'list_card_transactions', @@ -66,13 +75,19 @@ export const BrexBlock: BlockConfig = { // Budgets { label: 'List Budgets', id: 'list_budgets' }, { label: 'Get Budget', id: 'get_budget' }, + { label: 'Create Budget', id: 'create_budget' }, + { label: 'Archive Budget', id: 'archive_budget' }, { label: 'List Spend Limits', id: 'list_spend_limits' }, { label: 'Get Spend Limit', id: 'get_spend_limit' }, + { label: 'Create Spend Limit', id: 'create_spend_limit' }, // Payments { label: 'List Vendors', id: 'list_vendors' }, { label: 'Get Vendor', id: 'get_vendor' }, + { label: 'Create Vendor', id: 'create_vendor' }, + { label: 'Update Vendor', id: 'update_vendor' }, { label: 'List Transfers', id: 'list_transfers' }, { label: 'Get Transfer', id: 'get_transfer' }, + { label: 'Create Transfer', id: 'create_transfer' }, ], value: () => 'list_expenses', }, @@ -142,11 +157,16 @@ export const BrexBlock: BlockConfig = { placeholder: 'ID of the cash account (Get Cash Account defaults to primary)', condition: { field: 'operation', - value: ['list_cash_transactions', 'list_cash_statements', 'get_cash_account'], + value: [ + 'list_cash_transactions', + 'list_cash_statements', + 'get_cash_account', + 'create_transfer', + ], }, required: { field: 'operation', - value: ['list_cash_transactions', 'list_cash_statements'], + value: ['list_cash_transactions', 'list_cash_statements', 'create_transfer'], }, }, { @@ -162,8 +182,8 @@ export const BrexBlock: BlockConfig = { title: 'Budget ID', type: 'short-input', placeholder: 'ID of the budget', - condition: { field: 'operation', value: 'get_budget' }, - required: { field: 'operation', value: 'get_budget' }, + condition: { field: 'operation', value: ['get_budget', 'archive_budget'] }, + required: { field: 'operation', value: ['get_budget', 'archive_budget'] }, }, { id: 'spendLimitId', @@ -178,8 +198,8 @@ export const BrexBlock: BlockConfig = { title: 'Vendor ID', type: 'short-input', placeholder: 'ID of the vendor', - condition: { field: 'operation', value: 'get_vendor' }, - required: { field: 'operation', value: 'get_vendor' }, + condition: { field: 'operation', value: ['get_vendor', 'update_vendor'] }, + required: { field: 'operation', value: ['get_vendor', 'update_vendor'] }, }, { id: 'transferId', @@ -189,6 +209,305 @@ export const BrexBlock: BlockConfig = { condition: { field: 'operation', value: 'get_transfer' }, required: { field: 'operation', value: 'get_transfer' }, }, + { + id: 'companyName', + title: 'Company Name', + type: 'short-input', + placeholder: 'Name for the vendor (must be unique)', + condition: { field: 'operation', value: ['create_vendor', 'update_vendor'] }, + required: { field: 'operation', value: 'create_vendor' }, + }, + { + id: 'vendorEmail', + title: 'Email', + type: 'short-input', + placeholder: 'Email address for the vendor', + condition: { field: 'operation', value: ['create_vendor', 'update_vendor'] }, + }, + { + id: 'vendorPhone', + title: 'Phone', + type: 'short-input', + placeholder: 'Phone number for the vendor', + condition: { field: 'operation', value: ['create_vendor', 'update_vendor'] }, + }, + { + id: 'vendorPaymentInstrumentId', + title: 'Vendor Payment Instrument ID', + type: 'short-input', + placeholder: + "ID of the vendor's payment instrument to pay (from the vendor's payment accounts)", + condition: { field: 'operation', value: 'create_transfer' }, + required: { field: 'operation', value: 'create_transfer' }, + }, + { + id: 'amount', + title: 'Amount', + type: 'short-input', + placeholder: 'Amount in the smallest unit of the currency (e.g., cents for USD)', + condition: { field: 'operation', value: ['create_transfer', 'create_budget'] }, + required: { field: 'operation', value: ['create_transfer', 'create_budget'] }, + }, + { + id: 'currency', + title: 'Currency', + type: 'short-input', + placeholder: 'ISO 4217 currency code (defaults to USD)', + mode: 'advanced', + condition: { + field: 'operation', + value: ['create_transfer', 'create_budget', 'create_spend_limit'], + }, + }, + { + id: 'description', + title: 'Description', + type: 'long-input', + placeholder: 'Description of the transfer, budget, or spend limit', + condition: { + field: 'operation', + value: ['create_transfer', 'create_budget', 'create_spend_limit'], + }, + required: { field: 'operation', value: ['create_transfer', 'create_budget'] }, + }, + { + id: 'externalMemo', + title: 'External Memo', + type: 'short-input', + placeholder: 'Memo shown to the recipient (max 90 chars for ACH/Wire, 40 for Cheque)', + condition: { field: 'operation', value: 'create_transfer' }, + required: { field: 'operation', value: 'create_transfer' }, + }, + { + id: 'approvalType', + title: 'Approval Type', + type: 'dropdown', + options: [{ label: 'Manual approval required', id: 'MANUAL' }], + placeholder: 'Default policy applies if left blank', + mode: 'advanced', + condition: { field: 'operation', value: 'create_transfer' }, + }, + { + id: 'isPproEnabled', + title: 'Enable Principal Protection (PPRO)', + type: 'switch', + mode: 'advanced', + condition: { field: 'operation', value: 'create_transfer' }, + }, + { + id: 'resourceName', + title: 'Name', + type: 'short-input', + placeholder: 'Name for the budget or spend limit', + condition: { field: 'operation', value: ['create_budget', 'create_spend_limit'] }, + required: { field: 'operation', value: ['create_budget', 'create_spend_limit'] }, + }, + { + id: 'parentBudgetId', + title: 'Parent Budget ID', + type: 'short-input', + placeholder: 'ID of the parent budget', + condition: { field: 'operation', value: ['create_budget', 'create_spend_limit'] }, + required: { field: 'operation', value: 'create_budget' }, + }, + { + id: 'periodRecurrenceType', + title: 'Period Recurrence', + type: 'dropdown', + options: [ + { label: 'Weekly', id: 'WEEKLY' }, + { label: 'Monthly', id: 'MONTHLY' }, + { label: 'Quarterly', id: 'QUARTERLY' }, + { label: 'Yearly', id: 'YEARLY' }, + { label: 'One-time', id: 'ONE_TIME' }, + ], + condition: { field: 'operation', value: 'create_budget' }, + required: { field: 'operation', value: 'create_budget' }, + }, + { + id: 'startDate', + title: 'Start Date', + type: 'short-input', + placeholder: 'Date the budget/spend limit should start counting (YYYY-MM-DD)', + mode: 'advanced', + condition: { field: 'operation', value: ['create_budget', 'create_spend_limit'] }, + }, + { + id: 'endDate', + title: 'End Date', + type: 'short-input', + placeholder: 'Date the budget/spend limit should stop counting (YYYY-MM-DD)', + mode: 'advanced', + condition: { field: 'operation', value: ['create_budget', 'create_spend_limit'] }, + }, + { + id: 'ownerUserIds', + title: 'Owner User IDs', + type: 'short-input', + placeholder: 'Comma-separated user IDs of the budget/spend limit owners', + mode: 'advanced', + condition: { field: 'operation', value: ['create_budget', 'create_spend_limit'] }, + wandConfig: { + enabled: true, + prompt: + 'Generate a comma-separated list of Brex user IDs to set as owners based on the description.\n\nReturn ONLY the comma-separated user IDs - no explanations, no extra text.', + placeholder: 'Describe which users should own this budget/spend limit...', + }, + }, + { + id: 'spendLimitPeriodRecurrenceType', + title: 'Period Recurrence', + type: 'dropdown', + options: [ + { label: 'Per week', id: 'PER_WEEK' }, + { label: 'Per month', id: 'PER_MONTH' }, + { label: 'Per quarter', id: 'PER_QUARTER' }, + { label: 'Per year', id: 'PER_YEAR' }, + { label: 'One-time', id: 'ONE_TIME' }, + ], + condition: { field: 'operation', value: 'create_spend_limit' }, + required: { field: 'operation', value: 'create_spend_limit' }, + }, + { + id: 'spendType', + title: 'Spend Type', + type: 'dropdown', + options: [ + { label: 'Budget-provisioned cards only', id: 'BUDGET_PROVISIONED_CARDS_ONLY' }, + { + label: 'Non-budget-provisioned cards allowed', + id: 'NON_BUDGET_PROVISIONED_CARDS_ALLOWED', + }, + ], + condition: { field: 'operation', value: 'create_spend_limit' }, + required: { field: 'operation', value: 'create_spend_limit' }, + }, + { + id: 'expenseVisibility', + title: 'Expense Visibility', + type: 'dropdown', + options: [ + { label: 'Shared with all members', id: 'SHARED' }, + { label: 'Private', id: 'PRIVATE' }, + ], + condition: { field: 'operation', value: 'create_spend_limit' }, + required: { field: 'operation', value: 'create_spend_limit' }, + }, + { + id: 'authorizationVisibility', + title: 'Authorization Visibility', + type: 'dropdown', + options: [ + { label: 'Public to all members', id: 'PUBLIC' }, + { label: 'Private to controllers/owners', id: 'PRIVATE' }, + ], + condition: { field: 'operation', value: 'create_spend_limit' }, + required: { field: 'operation', value: 'create_spend_limit' }, + }, + { + id: 'limitIncreaseSetting', + title: 'Limit Increase Requests', + type: 'dropdown', + options: [ + { label: 'Enabled', id: 'ENABLED' }, + { label: 'Disabled', id: 'DISABLED' }, + ], + condition: { field: 'operation', value: 'create_spend_limit' }, + required: { field: 'operation', value: 'create_spend_limit' }, + }, + { + id: 'autoTransferCardsSetting', + title: 'Auto Transfer Cards', + type: 'dropdown', + options: [ + { label: 'Disabled', id: 'DISABLED' }, + { label: 'Enabled', id: 'ENABLED' }, + ], + condition: { field: 'operation', value: 'create_spend_limit' }, + required: { field: 'operation', value: 'create_spend_limit' }, + }, + { + id: 'autoCreateLimitCardsSetting', + title: 'Auto Create Limit Cards', + type: 'dropdown', + options: [ + { label: 'Disabled', id: 'DISABLED' }, + { label: 'All members', id: 'ALL_MEMBERS' }, + ], + condition: { field: 'operation', value: 'create_spend_limit' }, + required: { field: 'operation', value: 'create_spend_limit' }, + }, + { + id: 'expensePolicyId', + title: 'Expense Policy ID', + type: 'short-input', + placeholder: 'ID of the expense policy for this spend limit', + condition: { field: 'operation', value: 'create_spend_limit' }, + required: { field: 'operation', value: 'create_spend_limit' }, + }, + { + id: 'baseLimitAmount', + title: 'Base Limit Amount', + type: 'short-input', + placeholder: 'Base limit amount in the smallest unit of the currency (e.g., cents for USD)', + condition: { field: 'operation', value: 'create_spend_limit' }, + required: { field: 'operation', value: 'create_spend_limit' }, + }, + { + id: 'authorizationType', + title: 'Authorization Type', + type: 'dropdown', + options: [ + { label: 'Hard (declines over available balance)', id: 'HARD' }, + { label: 'Soft', id: 'SOFT' }, + ], + condition: { field: 'operation', value: 'create_spend_limit' }, + required: { field: 'operation', value: 'create_spend_limit' }, + }, + { + id: 'rolloverRefreshRate', + title: 'Rollover Refresh Rate', + type: 'dropdown', + options: [ + { label: 'Off', id: 'OFF' }, + { label: 'Never', id: 'NEVER' }, + { label: 'Per month', id: 'PER_MONTH' }, + { label: 'Per quarter', id: 'PER_QUARTER' }, + { label: 'Per year', id: 'PER_YEAR' }, + ], + condition: { field: 'operation', value: 'create_spend_limit' }, + required: { field: 'operation', value: 'create_spend_limit' }, + }, + { + id: 'limitBufferPercentage', + title: 'Limit Buffer Percentage', + type: 'short-input', + placeholder: 'Flexible buffer on the limit as a 0-100 percentage', + mode: 'advanced', + condition: { field: 'operation', value: 'create_spend_limit' }, + }, + { + id: 'transactionLimitAmount', + title: 'Transaction Limit Amount', + type: 'short-input', + placeholder: 'Per-transaction limit in the smallest unit of the currency', + mode: 'advanced', + condition: { field: 'operation', value: 'create_spend_limit' }, + }, + { + id: 'spendLimitMemberUserIds', + title: 'Member User IDs', + type: 'short-input', + placeholder: 'Comma-separated user IDs of the spend limit members', + mode: 'advanced', + condition: { field: 'operation', value: 'create_spend_limit' }, + wandConfig: { + enabled: true, + prompt: + 'Generate a comma-separated list of Brex user IDs to set as spend limit members based on the description.\n\nReturn ONLY the comma-separated user IDs - no explanations, no extra text.', + placeholder: 'Describe which users should be members of this spend limit...', + }, + }, { id: 'email', title: 'Email', @@ -388,12 +707,18 @@ export const BrexBlock: BlockConfig = { 'brex_get_company', 'brex_list_budgets', 'brex_get_budget', + 'brex_create_budget', + 'brex_archive_budget', 'brex_list_spend_limits', 'brex_get_spend_limit', + 'brex_create_spend_limit', 'brex_list_vendors', 'brex_get_vendor', + 'brex_create_vendor', + 'brex_update_vendor', 'brex_list_transfers', 'brex_get_transfer', + 'brex_create_transfer', ], config: { tool: (params) => `brex_${params.operation}`, @@ -459,15 +784,79 @@ export const BrexBlock: BlockConfig = { case 'get_budget': result.budgetId = params.budgetId break + case 'create_budget': { + result.name = params.resourceName + result.description = params.description + result.parentBudgetId = params.parentBudgetId + result.periodRecurrenceType = params.periodRecurrenceType + result.amount = toRequiredAmount(params.amount, 'Amount') + if (params.currency) result.currency = params.currency + if (params.ownerUserIds) result.ownerUserIds = params.ownerUserIds + if (params.startDate) result.startDate = params.startDate + if (params.endDate) result.endDate = params.endDate + break + } + case 'archive_budget': + result.budgetId = params.budgetId + break case 'get_spend_limit': result.spendLimitId = params.spendLimitId break + case 'create_spend_limit': { + result.name = params.resourceName + result.periodRecurrenceType = params.spendLimitPeriodRecurrenceType + result.spendType = params.spendType + result.expenseVisibility = params.expenseVisibility + result.authorizationVisibility = params.authorizationVisibility + result.limitIncreaseSetting = params.limitIncreaseSetting + result.autoTransferCardsSetting = params.autoTransferCardsSetting + result.autoCreateLimitCardsSetting = params.autoCreateLimitCardsSetting + result.expensePolicyId = params.expensePolicyId + result.baseLimitAmount = toRequiredAmount(params.baseLimitAmount, 'Base limit amount') + if (params.currency) result.currency = params.currency + result.authorizationType = params.authorizationType + result.rolloverRefreshRate = params.rolloverRefreshRate + if (params.limitBufferPercentage) + result.limitBufferPercentage = Number(params.limitBufferPercentage) + if (params.description) result.description = params.description + if (params.parentBudgetId) result.parentBudgetId = params.parentBudgetId + if (params.startDate) result.startDate = params.startDate + if (params.endDate) result.endDate = params.endDate + if (params.transactionLimitAmount) + result.transactionLimitAmount = Number(params.transactionLimitAmount) + if (params.ownerUserIds) result.ownerUserIds = params.ownerUserIds + if (params.spendLimitMemberUserIds) + result.memberUserIds = params.spendLimitMemberUserIds + break + } case 'get_vendor': result.vendorId = params.vendorId break + case 'create_vendor': + result.companyName = params.companyName + if (params.vendorEmail) result.email = params.vendorEmail + if (params.vendorPhone) result.phone = params.vendorPhone + break + case 'update_vendor': + result.vendorId = params.vendorId + if (params.companyName) result.companyName = params.companyName + if (params.vendorEmail) result.email = params.vendorEmail + if (params.vendorPhone) result.phone = params.vendorPhone + break case 'get_transfer': result.transferId = params.transferId break + case 'create_transfer': { + result.cashAccountId = params.accountId + result.vendorPaymentInstrumentId = params.vendorPaymentInstrumentId + result.amount = toRequiredAmount(params.amount, 'Amount') + if (params.currency) result.currency = params.currency + result.description = params.description + result.externalMemo = params.externalMemo + if (params.approvalType) result.approvalType = params.approvalType + if (params.isPproEnabled !== undefined) result.isPproEnabled = params.isPproEnabled + break + } default: break } @@ -508,6 +897,76 @@ export const BrexBlock: BlockConfig = { }, cursor: { type: 'string', description: 'Pagination cursor' }, limit: { type: 'string', description: 'Number of results to return' }, + companyName: { type: 'string', description: 'Vendor company name' }, + vendorEmail: { type: 'string', description: 'Vendor email address' }, + vendorPhone: { type: 'string', description: 'Vendor phone number' }, + vendorPaymentInstrumentId: { + type: 'string', + description: "Vendor's payment instrument ID for the transfer counterparty", + }, + amount: { type: 'string', description: 'Amount in the smallest unit of the currency' }, + currency: { type: 'string', description: 'ISO 4217 currency code' }, + description: { + type: 'string', + description: 'Description of the transfer, budget, or spend limit', + }, + externalMemo: { type: 'string', description: 'External memo shown to the transfer recipient' }, + approvalType: { type: 'string', description: 'Transfer approval type (MANUAL)' }, + isPproEnabled: { + type: 'boolean', + description: 'Whether to enable Principal Protection (PPRO) on the transfer', + }, + resourceName: { type: 'string', description: 'Name for the budget or spend limit' }, + parentBudgetId: { type: 'string', description: 'Parent budget ID' }, + periodRecurrenceType: { + type: 'string', + description: 'Budget period recurrence (WEEKLY, MONTHLY, QUARTERLY, YEARLY, ONE_TIME)', + }, + startDate: { type: 'string', description: 'Start date (YYYY-MM-DD)' }, + endDate: { type: 'string', description: 'End date (YYYY-MM-DD)' }, + ownerUserIds: { type: 'string', description: 'Comma-separated owner user IDs' }, + spendLimitPeriodRecurrenceType: { + type: 'string', + description: + 'Spend limit period recurrence (PER_WEEK, PER_MONTH, PER_QUARTER, PER_YEAR, ONE_TIME)', + }, + spendType: { type: 'string', description: 'Spend limit spend type' }, + expenseVisibility: { + type: 'string', + description: 'Spend limit expense visibility (SHARED, PRIVATE)', + }, + authorizationVisibility: { + type: 'string', + description: 'Spend limit authorization visibility (PUBLIC, PRIVATE)', + }, + limitIncreaseSetting: { + type: 'string', + description: 'Whether members can request limit increases', + }, + autoTransferCardsSetting: { + type: 'string', + description: 'Auto transfer setting for virtual cards', + }, + autoCreateLimitCardsSetting: { + type: 'string', + description: 'Auto limit card creation setting', + }, + expensePolicyId: { type: 'string', description: 'Expense policy ID for the spend limit' }, + baseLimitAmount: { + type: 'string', + description: 'Base spend limit amount before increases/rollovers', + }, + authorizationType: { + type: 'string', + description: 'Spend limit authorization type (HARD, SOFT)', + }, + rolloverRefreshRate: { type: 'string', description: 'Spend limit rollover refresh rate' }, + limitBufferPercentage: { type: 'string', description: 'Flexible buffer percentage (0-100)' }, + transactionLimitAmount: { type: 'string', description: 'Per-transaction limit amount' }, + spendLimitMemberUserIds: { + type: 'string', + description: 'Comma-separated member user IDs for a new spend limit', + }, }, outputs: { items: { type: 'json', description: 'Items returned by list operations' }, @@ -543,7 +1002,7 @@ export const BrexBlock: BlockConfig = { expenseId: { type: 'string', description: 'ID of the expense the receipt was attached to' }, firstName: { type: 'string', description: 'First name of the user' }, lastName: { type: 'string', description: 'Last name of the user' }, - email: { type: 'string', description: 'Email address of the user' }, + email: { type: 'string', description: 'Email address of the user or vendor' }, managerId: { type: 'string', description: 'Manager ID of the user' }, titleId: { type: 'string', description: 'Title ID of the user' }, legalName: { type: 'string', description: 'Legal name of the company' }, @@ -660,6 +1119,15 @@ export const BrexBlockMeta = { tags: ['automation'], alsoIntegrations: ['gmail'], }, + { + icon: BrexIcon, + title: 'Brex approved-invoice payment automation', + prompt: + 'Build a workflow that takes an approved invoice (vendor name, amount, and memo), creates the vendor in Brex if it does not already exist, and creates a transfer from the company cash account to pay it.', + modules: ['workflows'], + category: 'operations', + tags: ['automation'], + }, { icon: BrexIcon, title: 'Brex team directory assistant', @@ -717,5 +1185,11 @@ export const BrexBlockMeta = { content: '# Statement Reconciliation\n\nTie a card statement back to its underlying transactions.\n\n## Steps\n1. List card statements and pick the period to reconcile.\n2. List card transactions posted within that period using the posted-at filter.\n3. Compare transaction totals to the statement start and end balances and flag gaps.\n\n## Output\nReturn the statement period, its balances, the transaction total for the period, and any discrepancy that needs review.', }, + { + name: 'pay-vendor', + description: 'Pay a vendor from a Brex cash account, creating the vendor first if needed.', + content: + "# Pay a Vendor\n\nSend a payment to a vendor through Brex.\n\n## Steps\n1. List vendors and check if the target vendor already exists by name.\n2. If not, use Create Vendor with the company name (and email/phone if known).\n3. Use the vendor's payment_accounts entry to get the payment_instrument_id, then use Create Transfer with that ID, the cash account to pay from, the amount (in cents), a description, and an external memo.\n4. Confirm the transfer was created and note its status.\n\n## Output\nReturn the vendor used, the transfer ID and status, and the amount sent. Flag anything that requires manual approval (PENDING_APPROVAL status).", + }, ], } as const satisfies BlockMeta diff --git a/apps/sim/tools/brex/archive_budget.ts b/apps/sim/tools/brex/archive_budget.ts new file mode 100644 index 00000000000..1749e455d2f --- /dev/null +++ b/apps/sim/tools/brex/archive_budget.ts @@ -0,0 +1,71 @@ +import type { BrexArchiveBudgetParams, BrexArchiveBudgetResponse } from '@/tools/brex/types' +import { BREX_API_BASE, buildBrexHeaders, parseBrexJson } from '@/tools/brex/utils' +import type { ToolConfig } from '@/tools/types' + +export const brexArchiveBudgetTool: ToolConfig = + { + id: 'brex_archive_budget', + name: 'Brex Archive Budget', + description: + 'Archive a Brex budget, making any spend limits beneath it unusable for future expenses and removing it from the UI', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Brex user token (generated from Developer Settings in the Brex dashboard)', + }, + budgetId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'ID of the budget to archive', + }, + }, + + request: { + url: (params) => + `${BREX_API_BASE}/v2/budgets/${encodeURIComponent(params.budgetId.trim())}/archive`, + method: 'POST', + headers: (params) => buildBrexHeaders(params.apiKey), + }, + + transformResponse: async (response, params) => { + if (!response.ok) { + // parseBrexJson throws a descriptive error for non-2xx responses; it never + // returns in this branch since the body cannot be a successful JSON payload. + await parseBrexJson(response) + } + + // Brex's archive endpoint does not document a response body schema; fall back + // to the request's budget ID and an ARCHIVED status when the body is empty. + let data: Record = {} + const text = await response.text() + if (text) { + try { + data = JSON.parse(text) + } catch { + data = {} + } + } + + return { + success: true, + output: { + budgetId: (data.budget_id as string) ?? params?.budgetId ?? '', + spendBudgetStatus: (data.spend_budget_status as string) ?? 'ARCHIVED', + }, + } + }, + + outputs: { + budgetId: { type: 'string', description: 'ID of the archived budget' }, + spendBudgetStatus: { + type: 'string', + description: 'Status of the budget after archiving', + optional: true, + }, + }, + } diff --git a/apps/sim/tools/brex/create_budget.ts b/apps/sim/tools/brex/create_budget.ts new file mode 100644 index 00000000000..113e84c4dc9 --- /dev/null +++ b/apps/sim/tools/brex/create_budget.ts @@ -0,0 +1,145 @@ +import { generateId } from '@sim/utils/id' +import type { BrexCreateBudgetParams, BrexCreateBudgetResponse } from '@/tools/brex/types' +import { BREX_MONEY_PROPERTIES } from '@/tools/brex/types' +import { BREX_API_BASE, buildBrexHeaders, parseBrexJson, splitBrexIdList } from '@/tools/brex/utils' +import type { ToolConfig } from '@/tools/types' + +export const brexCreateBudgetTool: ToolConfig = { + id: 'brex_create_budget', + name: 'Brex Create Budget', + description: 'Create a new budget in the Brex account', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Brex user token (generated from Developer Settings in the Brex dashboard)', + }, + name: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Name for the budget', + }, + description: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Description of what the budget is used for', + }, + parentBudgetId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'ID of the parent budget', + }, + periodRecurrenceType: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Period type of the budget (WEEKLY, MONTHLY, QUARTERLY, YEARLY, ONE_TIME)', + }, + amount: { + type: 'number', + required: true, + visibility: 'user-or-llm', + description: 'Budget amount, in the smallest unit of the currency (e.g., cents for USD)', + }, + currency: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'ISO 4217 currency code (defaults to USD)', + }, + ownerUserIds: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Comma-separated user IDs of the budget owners', + }, + startDate: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Date the budget should start counting (YYYY-MM-DD)', + }, + endDate: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Date the budget should stop counting (YYYY-MM-DD)', + }, + }, + + request: { + url: () => `${BREX_API_BASE}/v2/budgets`, + method: 'POST', + headers: (params) => ({ + ...buildBrexHeaders(params.apiKey), + 'Idempotency-Key': generateId(), + }), + body: (params) => { + const body: Record = { + name: params.name, + description: params.description, + parent_budget_id: params.parentBudgetId, + period_recurrence_type: params.periodRecurrenceType, + amount: { + amount: params.amount, + currency: params.currency || 'USD', + }, + } + const ownerUserIds = splitBrexIdList(params.ownerUserIds) + if (ownerUserIds) body.owner_user_ids = ownerUserIds + if (params.startDate) body.start_date = params.startDate + if (params.endDate) body.end_date = params.endDate + return body + }, + }, + + transformResponse: async (response) => { + const data = await parseBrexJson(response) + return { + success: true, + output: { + budgetId: data.budget_id ?? '', + accountId: data.account_id ?? '', + name: data.name ?? '', + description: data.description ?? null, + parentBudgetId: data.parent_budget_id ?? null, + ownerUserIds: data.owner_user_ids ?? [], + periodRecurrenceType: data.period_recurrence_type ?? '', + startDate: data.start_date ?? null, + endDate: data.end_date ?? null, + amount: data.amount ?? null, + spendBudgetStatus: data.spend_budget_status ?? '', + limitType: data.limit_type ?? null, + }, + } + }, + + outputs: { + budgetId: { type: 'string', description: 'Unique budget ID' }, + accountId: { type: 'string', description: 'Account ID the budget belongs to' }, + name: { type: 'string', description: 'Budget name' }, + description: { type: 'string', description: 'Budget description', optional: true }, + parentBudgetId: { type: 'string', description: 'Parent budget ID', optional: true }, + ownerUserIds: { type: 'array', description: 'User IDs of the budget owners' }, + periodRecurrenceType: { + type: 'string', + description: 'Budget period recurrence (WEEKLY, MONTHLY, QUARTERLY, YEARLY, ONE_TIME)', + }, + startDate: { type: 'string', description: 'Budget start date', optional: true }, + endDate: { type: 'string', description: 'Budget end date', optional: true }, + amount: { + type: 'json', + description: 'Budget amount', + optional: true, + properties: BREX_MONEY_PROPERTIES, + }, + spendBudgetStatus: { type: 'string', description: 'Status of the created budget' }, + limitType: { type: 'string', description: 'Budget limit type', optional: true }, + }, +} diff --git a/apps/sim/tools/brex/create_spend_limit.ts b/apps/sim/tools/brex/create_spend_limit.ts new file mode 100644 index 00000000000..fde9df62d41 --- /dev/null +++ b/apps/sim/tools/brex/create_spend_limit.ts @@ -0,0 +1,256 @@ +import { generateId } from '@sim/utils/id' +import type { BrexCreateSpendLimitParams, BrexCreateSpendLimitResponse } from '@/tools/brex/types' +import { BREX_SPEND_LIMIT_PERIOD_BALANCE_PROPERTIES } from '@/tools/brex/types' +import { BREX_API_BASE, buildBrexHeaders, parseBrexJson, splitBrexIdList } from '@/tools/brex/utils' +import type { ToolConfig } from '@/tools/types' + +export const brexCreateSpendLimitTool: ToolConfig< + BrexCreateSpendLimitParams, + BrexCreateSpendLimitResponse +> = { + id: 'brex_create_spend_limit', + name: 'Brex Create Spend Limit', + description: 'Create a new spend limit (hard-authorization card program) in the Brex account', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Brex user token (generated from Developer Settings in the Brex dashboard)', + }, + name: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Name for the spend limit', + }, + periodRecurrenceType: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: + 'Period type of the spend limit (PER_WEEK, PER_MONTH, PER_QUARTER, PER_YEAR, ONE_TIME)', + }, + spendType: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: + 'Whether the spend limit can only be spent from cards it provisions (BUDGET_PROVISIONED_CARDS_ONLY, NON_BUDGET_PROVISIONED_CARDS_ALLOWED)', + }, + expenseVisibility: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: + 'Whether expenses on this spend limit are viewable by all members (SHARED, PRIVATE)', + }, + authorizationVisibility: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: + 'Whether the limit amount is visible to all members, or just controllers/bookkeepers/owners (PUBLIC, PRIVATE)', + }, + limitIncreaseSetting: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Whether members can request limit increases (ENABLED, DISABLED)', + }, + autoTransferCardsSetting: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: + 'How auto transfer works for virtual cards on this spend limit (DISABLED, ENABLED)', + }, + autoCreateLimitCardsSetting: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'How auto limit card creation works for members (DISABLED, ALL_MEMBERS)', + }, + expensePolicyId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'ID of the expense policy corresponding to this spend limit', + }, + baseLimitAmount: { + type: 'number', + required: true, + visibility: 'user-or-llm', + description: + 'Base spend limit amount, without increases/rollovers, in the smallest unit of the currency (e.g., cents for USD)', + }, + currency: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'ISO 4217 currency code for the base limit (defaults to USD)', + }, + authorizationType: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Whether authorizations decline based on available balance (HARD, SOFT)', + }, + rolloverRefreshRate: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: + 'Recurrence at which rolled-over unused funds stop rolling over (OFF, NEVER, PER_MONTH, PER_QUARTER, PER_YEAR)', + }, + limitBufferPercentage: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Flexible buffer on the limit as a 0-100 percentage', + }, + description: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Description of what the spend limit is used for', + }, + parentBudgetId: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'ID of the parent budget', + }, + startDate: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Date the spend limit should start counting (YYYY-MM-DD)', + }, + endDate: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Date the spend limit should expire (YYYY-MM-DD)', + }, + transactionLimitAmount: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: + 'Per-transaction limit this spend limit enforces, in the smallest unit of the currency', + }, + ownerUserIds: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Comma-separated user IDs of the spend limit owners', + }, + memberUserIds: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Comma-separated user IDs of the spend limit members', + }, + }, + + request: { + url: () => `${BREX_API_BASE}/v2/spend_limits`, + method: 'POST', + headers: (params) => ({ + ...buildBrexHeaders(params.apiKey), + 'Idempotency-Key': generateId(), + }), + body: (params) => { + const currency = params.currency || 'USD' + const body: Record = { + name: params.name, + period_recurrence_type: params.periodRecurrenceType, + spend_type: params.spendType, + expense_visibility: params.expenseVisibility, + authorization_visibility: params.authorizationVisibility, + limit_increase_setting: params.limitIncreaseSetting, + auto_transfer_cards_setting: params.autoTransferCardsSetting, + auto_create_limit_cards_setting: params.autoCreateLimitCardsSetting, + expense_policy_id: params.expensePolicyId, + authorization_settings: { + base_limit: { + amount: params.baseLimitAmount, + currency, + }, + authorization_type: params.authorizationType, + rollover_refresh_rate: params.rolloverRefreshRate, + ...(params.limitBufferPercentage !== undefined + ? { limit_buffer_percentage: params.limitBufferPercentage } + : {}), + }, + } + if (params.description) body.description = params.description + if (params.parentBudgetId) body.parent_budget_id = params.parentBudgetId + if (params.startDate) body.start_date = params.startDate + if (params.endDate) body.end_date = params.endDate + if (params.transactionLimitAmount !== undefined) { + body.transaction_limit = { amount: params.transactionLimitAmount, currency } + } + const ownerUserIds = splitBrexIdList(params.ownerUserIds) + if (ownerUserIds) body.owner_user_ids = ownerUserIds + const memberUserIds = splitBrexIdList(params.memberUserIds) + if (memberUserIds) body.member_user_ids = memberUserIds + return body + }, + }, + + transformResponse: async (response) => { + const data = await parseBrexJson(response) + return { + success: true, + output: { + id: data.id ?? '', + accountId: data.account_id ?? '', + name: data.name ?? '', + description: data.description ?? null, + parentBudgetId: data.parent_budget_id ?? null, + status: data.status ?? '', + periodRecurrenceType: data.period_recurrence_type ?? '', + spendType: data.spend_type ?? '', + startDate: data.start_date ?? null, + endDate: data.end_date ?? null, + ownerUserIds: data.owner_user_ids ?? [], + memberUserIds: data.member_user_ids ?? [], + currentPeriodBalance: data.current_period_balance ?? null, + authorizationSettings: data.authorization_settings ?? null, + }, + } + }, + + outputs: { + id: { type: 'string', description: 'Unique spend limit ID' }, + accountId: { type: 'string', description: 'Account ID the spend limit belongs to' }, + name: { type: 'string', description: 'Spend limit name' }, + description: { type: 'string', description: 'Spend limit description', optional: true }, + parentBudgetId: { type: 'string', description: 'Parent budget ID', optional: true }, + status: { type: 'string', description: 'Spend limit status' }, + periodRecurrenceType: { + type: 'string', + description: 'Period recurrence (PER_WEEK, PER_MONTH, PER_QUARTER, PER_YEAR, ONE_TIME)', + }, + spendType: { type: 'string', description: 'Spend type of the limit' }, + startDate: { type: 'string', description: 'Spend limit start date', optional: true }, + endDate: { type: 'string', description: 'Spend limit end date', optional: true }, + ownerUserIds: { type: 'array', description: 'User IDs of the spend limit owners' }, + memberUserIds: { type: 'array', description: 'User IDs of the spend limit members' }, + currentPeriodBalance: { + type: 'json', + description: 'Spend and rollover amounts for the current period', + optional: true, + properties: BREX_SPEND_LIMIT_PERIOD_BALANCE_PROPERTIES, + }, + authorizationSettings: { + type: 'json', + description: 'Authorization settings (base limit, authorization type, rollover refresh)', + optional: true, + }, + }, +} diff --git a/apps/sim/tools/brex/create_transfer.ts b/apps/sim/tools/brex/create_transfer.ts new file mode 100644 index 00000000000..5c4dc7a9689 --- /dev/null +++ b/apps/sim/tools/brex/create_transfer.ts @@ -0,0 +1,189 @@ +import { generateId } from '@sim/utils/id' +import type { BrexCreateTransferParams, BrexCreateTransferResponse } from '@/tools/brex/types' +import { BREX_MONEY_PROPERTIES } from '@/tools/brex/types' +import { BREX_API_BASE, buildBrexHeaders, parseBrexJson } from '@/tools/brex/utils' +import type { ToolConfig } from '@/tools/types' + +export const brexCreateTransferTool: ToolConfig< + BrexCreateTransferParams, + BrexCreateTransferResponse +> = { + id: 'brex_create_transfer', + name: 'Brex Create Transfer', + description: 'Create a money transfer from a Brex cash account to a vendor', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Brex user token (generated from Developer Settings in the Brex dashboard)', + }, + cashAccountId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: + 'ID of the Brex cash account to send the transfer from (found via the /accounts endpoint)', + }, + vendorPaymentInstrumentId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: + "ID of the vendor's payment instrument to send the transfer to (from the vendor's payment_accounts)", + }, + amount: { + type: 'number', + required: true, + visibility: 'user-or-llm', + description: 'Amount to transfer, in the smallest unit of the currency (e.g., cents for USD)', + }, + currency: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'ISO 4217 currency code (defaults to USD)', + }, + description: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Description of the transfer for internal use (not exposed externally)', + }, + externalMemo: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: + 'External memo shown to the recipient (max 90 characters for ACH/Wire, 40 for Cheque)', + }, + approvalType: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Set to MANUAL to require cash admin approval before the transfer is sent', + }, + isPproEnabled: { + type: 'boolean', + required: false, + visibility: 'user-or-llm', + description: + 'Enable Principal Protection (PPRO) to have Brex cover intermediary/receiving bank fees (international wires only)', + }, + }, + + request: { + url: () => `${BREX_API_BASE}/v1/transfers`, + method: 'POST', + headers: (params) => ({ + ...buildBrexHeaders(params.apiKey), + // Brex requires a fresh Idempotency-Key per transfer creation to prevent duplicate money movement. + 'Idempotency-Key': generateId(), + }), + body: (params) => { + const body: Record = { + counterparty: { + type: 'VENDOR', + payment_instrument_id: params.vendorPaymentInstrumentId, + }, + amount: { + amount: params.amount, + currency: params.currency || 'USD', + }, + description: params.description, + external_memo: params.externalMemo, + originating_account: { + type: 'BREX_CASH', + id: params.cashAccountId, + }, + } + if (params.approvalType) body.approval_type = params.approvalType + if (params.isPproEnabled !== undefined) body.is_ppro_enabled = params.isPproEnabled + return body + }, + }, + + transformResponse: async (response) => { + const data = await parseBrexJson(response) + return { + success: true, + output: { + id: data.id ?? '', + counterparty: data.counterparty ?? null, + description: data.description ?? null, + paymentType: data.payment_type ?? '', + amount: data.amount ?? null, + processDate: data.process_date ?? null, + originatingAccount: data.originating_account ?? null, + status: data.status ?? '', + cancellationReason: data.cancellation_reason ?? null, + estimatedDeliveryDate: data.estimated_delivery_date ?? null, + creatorUserId: data.creator_user_id ?? null, + createdAt: data.created_at ?? null, + displayName: data.display_name ?? null, + externalMemo: data.external_memo ?? null, + isPproEnabled: data.is_ppro_enabled ?? null, + }, + } + }, + + outputs: { + id: { type: 'string', description: 'Unique transfer ID' }, + counterparty: { type: 'json', description: 'Transfer counterparty details', optional: true }, + description: { type: 'string', description: 'Description of the transfer', optional: true }, + paymentType: { + type: 'string', + description: + 'Payment type (ACH, DOMESTIC_WIRE, CHEQUE, INTERNATIONAL_WIRE, BOOK_TRANSFER, STABLECOIN)', + }, + amount: { + type: 'json', + description: 'Transfer amount', + optional: true, + properties: BREX_MONEY_PROPERTIES, + }, + processDate: { type: 'string', description: 'Transaction processing date', optional: true }, + originatingAccount: { + type: 'json', + description: 'Originating account details for the transfer', + optional: true, + }, + status: { + type: 'string', + description: 'Transfer status (PROCESSING, SCHEDULED, PENDING_APPROVAL, FAILED, PROCESSED)', + }, + cancellationReason: { + type: 'string', + description: 'Reason the transfer was canceled', + optional: true, + }, + estimatedDeliveryDate: { + type: 'string', + description: 'Estimated delivery date for the transfer', + optional: true, + }, + creatorUserId: { + type: 'string', + description: 'ID of the user who created the transfer', + optional: true, + }, + createdAt: { + type: 'string', + description: 'Creation timestamp of the transfer', + optional: true, + }, + displayName: { + type: 'string', + description: 'Human-readable name of the transfer', + optional: true, + }, + externalMemo: { type: 'string', description: 'External memo of the transfer', optional: true }, + isPproEnabled: { + type: 'boolean', + description: 'Whether Principal Protection (PPRO) is enabled for the transfer', + optional: true, + }, + }, +} diff --git a/apps/sim/tools/brex/create_vendor.ts b/apps/sim/tools/brex/create_vendor.ts new file mode 100644 index 00000000000..9b485eddba1 --- /dev/null +++ b/apps/sim/tools/brex/create_vendor.ts @@ -0,0 +1,75 @@ +import { generateId } from '@sim/utils/id' +import type { BrexCreateVendorParams, BrexCreateVendorResponse } from '@/tools/brex/types' +import { BREX_API_BASE, buildBrexHeaders, parseBrexJson } from '@/tools/brex/utils' +import type { ToolConfig } from '@/tools/types' + +export const brexCreateVendorTool: ToolConfig = { + id: 'brex_create_vendor', + name: 'Brex Create Vendor', + description: 'Create a new vendor in the Brex account', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Brex user token (generated from Developer Settings in the Brex dashboard)', + }, + companyName: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Name for the vendor (must be unique)', + }, + email: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Email address for the vendor', + }, + phone: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Phone number for the vendor', + }, + }, + + request: { + url: () => `${BREX_API_BASE}/v1/vendors`, + method: 'POST', + headers: (params) => ({ + ...buildBrexHeaders(params.apiKey), + 'Idempotency-Key': generateId(), + }), + body: (params) => { + const body: Record = { company_name: params.companyName } + if (params.email) body.email = params.email + if (params.phone) body.phone = params.phone + return body + }, + }, + + transformResponse: async (response) => { + const data = await parseBrexJson(response) + return { + success: true, + output: { + id: data.id ?? '', + companyName: data.company_name ?? null, + email: data.email ?? null, + phone: data.phone ?? null, + paymentAccounts: data.payment_accounts ?? [], + }, + } + }, + + outputs: { + id: { type: 'string', description: 'Unique vendor ID' }, + companyName: { type: 'string', description: 'Vendor company name', optional: true }, + email: { type: 'string', description: 'Vendor email address', optional: true }, + phone: { type: 'string', description: 'Vendor phone number', optional: true }, + paymentAccounts: { type: 'array', description: 'Payment accounts associated with the vendor' }, + }, +} diff --git a/apps/sim/tools/brex/index.ts b/apps/sim/tools/brex/index.ts index 4bc1194ee08..0719c1784b5 100644 --- a/apps/sim/tools/brex/index.ts +++ b/apps/sim/tools/brex/index.ts @@ -1,3 +1,8 @@ +export { brexArchiveBudgetTool } from '@/tools/brex/archive_budget' +export { brexCreateBudgetTool } from '@/tools/brex/create_budget' +export { brexCreateSpendLimitTool } from '@/tools/brex/create_spend_limit' +export { brexCreateTransferTool } from '@/tools/brex/create_transfer' +export { brexCreateVendorTool } from '@/tools/brex/create_vendor' export { brexGetBudgetTool } from '@/tools/brex/get_budget' export { brexGetCashAccountTool } from '@/tools/brex/get_cash_account' export { brexGetCompanyTool } from '@/tools/brex/get_company' @@ -25,4 +30,5 @@ export { brexListUsersTool } from '@/tools/brex/list_users' export { brexListVendorsTool } from '@/tools/brex/list_vendors' export { brexMatchReceiptTool } from '@/tools/brex/match_receipt' export { brexUpdateExpenseTool } from '@/tools/brex/update_expense' +export { brexUpdateVendorTool } from '@/tools/brex/update_vendor' export { brexUploadReceiptTool } from '@/tools/brex/upload_receipt' diff --git a/apps/sim/tools/brex/types.ts b/apps/sim/tools/brex/types.ts index f0c14b13927..a79211afc85 100644 --- a/apps/sim/tools/brex/types.ts +++ b/apps/sim/tools/brex/types.ts @@ -299,6 +299,76 @@ export interface BrexGetTransferParams { transferId: string } +export interface BrexCreateTransferParams { + apiKey: string + cashAccountId: string + vendorPaymentInstrumentId: string + amount: number + currency?: string + description: string + externalMemo: string + approvalType?: string + isPproEnabled?: boolean +} + +export interface BrexCreateBudgetParams { + apiKey: string + name: string + description: string + parentBudgetId: string + periodRecurrenceType: string + amount: number + currency?: string + ownerUserIds?: string + startDate?: string + endDate?: string +} + +export interface BrexArchiveBudgetParams { + apiKey: string + budgetId: string +} + +export interface BrexCreateSpendLimitParams { + apiKey: string + name: string + periodRecurrenceType: string + spendType: string + expenseVisibility: string + authorizationVisibility: string + limitIncreaseSetting: string + autoTransferCardsSetting: string + autoCreateLimitCardsSetting: string + expensePolicyId: string + baseLimitAmount: number + currency?: string + authorizationType: string + rolloverRefreshRate: string + limitBufferPercentage?: number + description?: string + parentBudgetId?: string + startDate?: string + endDate?: string + transactionLimitAmount?: number + ownerUserIds?: string + memberUserIds?: string +} + +export interface BrexCreateVendorParams { + apiKey: string + companyName: string + email?: string + phone?: string +} + +export interface BrexUpdateVendorParams { + apiKey: string + vendorId: string + companyName?: string + email?: string + phone?: string +} + export interface BrexListExpensesResponse extends ToolResponse { output: { items: BrexExpense[] @@ -559,6 +629,89 @@ export interface BrexGetTransferResponse extends ToolResponse { } } +export interface BrexCreateTransferResponse extends ToolResponse { + output: { + id: string + counterparty: Record | null + description: string | null + paymentType: string + amount: BrexMoney | null + processDate: string | null + originatingAccount: Record | null + status: string + cancellationReason: string | null + estimatedDeliveryDate: string | null + creatorUserId: string | null + createdAt: string | null + displayName: string | null + externalMemo: string | null + isPproEnabled: boolean | null + } +} + +export interface BrexCreateBudgetResponse extends ToolResponse { + output: { + budgetId: string + accountId: string + name: string + description: string | null + parentBudgetId: string | null + ownerUserIds: string[] + periodRecurrenceType: string + startDate: string | null + endDate: string | null + amount: BrexMoney | null + spendBudgetStatus: string + limitType: string | null + } +} + +export interface BrexArchiveBudgetResponse extends ToolResponse { + output: { + budgetId: string + spendBudgetStatus: string | null + } +} + +export interface BrexCreateSpendLimitResponse extends ToolResponse { + output: { + id: string + accountId: string + name: string + description: string | null + parentBudgetId: string | null + status: string + periodRecurrenceType: string + spendType: string + startDate: string | null + endDate: string | null + ownerUserIds: string[] + memberUserIds: string[] + currentPeriodBalance: BrexSpendLimitPeriodBalance | null + authorizationSettings: Record | null + } +} + +export interface BrexCreateVendorResponse extends ToolResponse { + output: { + id: string + companyName: string | null + email: string | null + phone: string | null + paymentAccounts: unknown[] + } +} + +export interface BrexUpdateVendorResponse extends ToolResponse { + output: { + id: string + companyName: string | null + email: string | null + phone: string | null + paymentAccounts: unknown[] + } +} + export type BrexResponse = | BrexListExpensesResponse | BrexGetExpenseResponse @@ -585,6 +738,12 @@ export type BrexResponse = | BrexGetSpendLimitResponse | BrexGetVendorResponse | BrexGetTransferResponse + | BrexCreateTransferResponse + | BrexCreateBudgetResponse + | BrexArchiveBudgetResponse + | BrexCreateSpendLimitResponse + | BrexCreateVendorResponse + | BrexUpdateVendorResponse export const BREX_MONEY_PROPERTIES: Record = { amount: { diff --git a/apps/sim/tools/brex/update_vendor.ts b/apps/sim/tools/brex/update_vendor.ts new file mode 100644 index 00000000000..7c55049b858 --- /dev/null +++ b/apps/sim/tools/brex/update_vendor.ts @@ -0,0 +1,83 @@ +import { generateId } from '@sim/utils/id' +import type { BrexUpdateVendorParams, BrexUpdateVendorResponse } from '@/tools/brex/types' +import { BREX_API_BASE, buildBrexHeaders, parseBrexJson } from '@/tools/brex/utils' +import type { ToolConfig } from '@/tools/types' + +export const brexUpdateVendorTool: ToolConfig = { + id: 'brex_update_vendor', + name: 'Brex Update Vendor', + description: 'Update an existing vendor in the Brex account', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Brex user token (generated from Developer Settings in the Brex dashboard)', + }, + vendorId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'ID of the vendor to update', + }, + companyName: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'New name for the vendor', + }, + email: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'New email address for the vendor', + }, + phone: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'New phone number for the vendor', + }, + }, + + request: { + url: (params) => `${BREX_API_BASE}/v1/vendors/${encodeURIComponent(params.vendorId.trim())}`, + method: 'PUT', + headers: (params) => ({ + ...buildBrexHeaders(params.apiKey), + // Optional per Brex's spec for this endpoint, but included for safe-retry semantics. + 'Idempotency-Key': generateId(), + }), + body: (params) => { + const body: Record = {} + if (params.companyName) body.company_name = params.companyName + if (params.email) body.email = params.email + if (params.phone) body.phone = params.phone + return body + }, + }, + + transformResponse: async (response) => { + const data = await parseBrexJson(response) + return { + success: true, + output: { + id: data.id ?? '', + companyName: data.company_name ?? null, + email: data.email ?? null, + phone: data.phone ?? null, + paymentAccounts: data.payment_accounts ?? [], + }, + } + }, + + outputs: { + id: { type: 'string', description: 'Unique vendor ID' }, + companyName: { type: 'string', description: 'Vendor company name', optional: true }, + email: { type: 'string', description: 'Vendor email address', optional: true }, + phone: { type: 'string', description: 'Vendor phone number', optional: true }, + paymentAccounts: { type: 'array', description: 'Payment accounts associated with the vendor' }, + }, +} diff --git a/apps/sim/tools/brex/utils.ts b/apps/sim/tools/brex/utils.ts index 19edd9bb79b..2530557f231 100644 --- a/apps/sim/tools/brex/utils.ts +++ b/apps/sim/tools/brex/utils.ts @@ -51,6 +51,19 @@ export function appendBrexPagination( if (params.limit) query.append('limit', params.limit) } +/** + * Splits a comma-separated string of IDs into a trimmed, non-empty array for + * use in a JSON request body (as opposed to repeated query parameters). + */ +export function splitBrexIdList(value?: string): string[] | undefined { + if (!value) return undefined + const ids = value + .split(',') + .map((id) => id.trim()) + .filter(Boolean) + return ids.length > 0 ? ids : undefined +} + /** * Converts a timestamp to the timezone-less date-time form the Brex Transactions * API requires (e.g., 2026-01-01T00:00:00). Brex rejects timezone-suffixed diff --git a/apps/sim/tools/registry.ts b/apps/sim/tools/registry.ts index 4c2ce2bf0d9..e9423a2837b 100644 --- a/apps/sim/tools/registry.ts +++ b/apps/sim/tools/registry.ts @@ -345,6 +345,11 @@ import { } from '@/tools/box_sign' import { brandfetchGetBrandTool, brandfetchSearchTool } from '@/tools/brandfetch' import { + brexArchiveBudgetTool, + brexCreateBudgetTool, + brexCreateSpendLimitTool, + brexCreateTransferTool, + brexCreateVendorTool, brexGetBudgetTool, brexGetCashAccountTool, brexGetCompanyTool, @@ -372,6 +377,7 @@ import { brexListVendorsTool, brexMatchReceiptTool, brexUpdateExpenseTool, + brexUpdateVendorTool, brexUploadReceiptTool, } from '@/tools/brex' import { @@ -4632,6 +4638,11 @@ export const tools: Record = { athena_stop_query: athenaStopQueryTool, brandfetch_get_brand: brandfetchGetBrandTool, brandfetch_search: brandfetchSearchTool, + brex_archive_budget: brexArchiveBudgetTool, + brex_create_budget: brexCreateBudgetTool, + brex_create_spend_limit: brexCreateSpendLimitTool, + brex_create_transfer: brexCreateTransferTool, + brex_create_vendor: brexCreateVendorTool, brex_get_budget: brexGetBudgetTool, brex_get_cash_account: brexGetCashAccountTool, brex_get_company: brexGetCompanyTool, @@ -4659,6 +4670,7 @@ export const tools: Record = { brex_list_vendors: brexListVendorsTool, brex_match_receipt: brexMatchReceiptTool, brex_update_expense: brexUpdateExpenseTool, + brex_update_vendor: brexUpdateVendorTool, brex_upload_receipt: brexUploadReceiptTool, brightdata_cancel_snapshot: brightDataCancelSnapshotTool, brightdata_discover: brightDataDiscoverTool, From fcac1195ef6ec7f8917306214ace2c7b061dce9c Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Thu, 9 Jul 2026 20:50:25 -0700 Subject: [PATCH 2/4] fix(brex): correct status enum docs and normalize expense date filters - get_budget/get_spend_limit output descriptions listed status enum values not in Brex's actual schema - list_expenses now normalizes purchased_at_start/end through toBrexDateTime for consistency with list_card_transactions/list_cash_transactions --- apps/sim/tools/brex/get_budget.ts | 2 +- apps/sim/tools/brex/get_spend_limit.ts | 2 +- apps/sim/tools/brex/list_expenses.ts | 7 +++++-- 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/apps/sim/tools/brex/get_budget.ts b/apps/sim/tools/brex/get_budget.ts index 9496eac14cc..d00bebd1b48 100644 --- a/apps/sim/tools/brex/get_budget.ts +++ b/apps/sim/tools/brex/get_budget.ts @@ -72,7 +72,7 @@ export const brexGetBudgetTool: ToolConfig Date: Thu, 9 Jul 2026 20:51:54 -0700 Subject: [PATCH 3/4] fix(brex): preserve zero values and normalize booleans in write params - toRequiredAmount now rejects whitespace-only input (Number(' ') coerces to 0) - limitBufferPercentage/transactionLimitAmount used truthy checks that dropped explicit 0 - isPproEnabled now normalized to a real boolean instead of forwarding a stringified 'false' from dynamic references --- apps/sim/blocks/blocks/brex.ts | 41 +++++++++++++++++++++++++++++----- 1 file changed, 35 insertions(+), 6 deletions(-) diff --git a/apps/sim/blocks/blocks/brex.ts b/apps/sim/blocks/blocks/brex.ts index 7c7f77223ca..92d1f237d1f 100644 --- a/apps/sim/blocks/blocks/brex.ts +++ b/apps/sim/blocks/blocks/brex.ts @@ -6,13 +6,33 @@ import type { BrexResponse } from '@/tools/brex/types' /** Coerces a required money-amount field to a finite number, throwing on blank/non-numeric input rather than silently sending 0 or NaN to Brex. */ function toRequiredAmount(value: unknown, fieldLabel: string): number { + if (value == null || (typeof value === 'string' && value.trim() === '')) { + throw new Error(`${fieldLabel} must be a valid number`) + } + const parsed = Number(value) + if (!Number.isFinite(parsed)) { + throw new Error(`${fieldLabel} must be a valid number`) + } + return parsed +} + +/** Coerces an optional numeric field to a finite number, throwing on non-numeric input instead of silently forwarding NaN. Preserves explicit 0. */ +function toOptionalFiniteNumber(value: unknown, fieldLabel: string): number | undefined { + if (value == null || (typeof value === 'string' && value.trim() === '')) return undefined const parsed = Number(value) - if (value === '' || value == null || !Number.isFinite(parsed)) { + if (!Number.isFinite(parsed)) { throw new Error(`${fieldLabel} must be a valid number`) } return parsed } +/** Normalizes a boolean field that may arrive as a string (e.g. from a dynamic reference) instead of an actual boolean. */ +function toOptionalBoolean(value: unknown): boolean | undefined { + if (value === undefined) return undefined + if (typeof value === 'boolean') return value + return String(value).toLowerCase() === 'true' +} + const PAGINATED_OPERATIONS = new Set([ 'list_expenses', 'list_card_transactions', @@ -816,14 +836,22 @@ export const BrexBlock: BlockConfig = { if (params.currency) result.currency = params.currency result.authorizationType = params.authorizationType result.rolloverRefreshRate = params.rolloverRefreshRate - if (params.limitBufferPercentage) - result.limitBufferPercentage = Number(params.limitBufferPercentage) + const limitBufferPercentage = toOptionalFiniteNumber( + params.limitBufferPercentage, + 'Limit buffer percentage' + ) + if (limitBufferPercentage !== undefined) + result.limitBufferPercentage = limitBufferPercentage if (params.description) result.description = params.description if (params.parentBudgetId) result.parentBudgetId = params.parentBudgetId if (params.startDate) result.startDate = params.startDate if (params.endDate) result.endDate = params.endDate - if (params.transactionLimitAmount) - result.transactionLimitAmount = Number(params.transactionLimitAmount) + const transactionLimitAmount = toOptionalFiniteNumber( + params.transactionLimitAmount, + 'Transaction limit amount' + ) + if (transactionLimitAmount !== undefined) + result.transactionLimitAmount = transactionLimitAmount if (params.ownerUserIds) result.ownerUserIds = params.ownerUserIds if (params.spendLimitMemberUserIds) result.memberUserIds = params.spendLimitMemberUserIds @@ -854,7 +882,8 @@ export const BrexBlock: BlockConfig = { result.description = params.description result.externalMemo = params.externalMemo if (params.approvalType) result.approvalType = params.approvalType - if (params.isPproEnabled !== undefined) result.isPproEnabled = params.isPproEnabled + const pproEnabled = toOptionalBoolean(params.isPproEnabled) + if (pproEnabled !== undefined) result.isPproEnabled = pproEnabled break } default: From 0103fc14341904ce28ea1207a24923a779f470f6 Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Thu, 9 Jul 2026 20:57:03 -0700 Subject: [PATCH 4/4] fix(brex): null-safe PPRO coercion and fail-fast on empty vendor update - toOptionalBoolean now treats null the same as undefined (was only checking undefined), so a null isPproEnabled from a dynamic reference is omitted instead of coerced to false - brex_update_vendor throws when no updatable field (companyName/email/phone) is provided, instead of sending an empty PUT body --- apps/sim/blocks/blocks/brex.ts | 2 +- apps/sim/tools/brex/update_vendor.ts | 5 +++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/apps/sim/blocks/blocks/brex.ts b/apps/sim/blocks/blocks/brex.ts index 92d1f237d1f..086617b2615 100644 --- a/apps/sim/blocks/blocks/brex.ts +++ b/apps/sim/blocks/blocks/brex.ts @@ -28,7 +28,7 @@ function toOptionalFiniteNumber(value: unknown, fieldLabel: string): number | un /** Normalizes a boolean field that may arrive as a string (e.g. from a dynamic reference) instead of an actual boolean. */ function toOptionalBoolean(value: unknown): boolean | undefined { - if (value === undefined) return undefined + if (value == null) return undefined if (typeof value === 'boolean') return value return String(value).toLowerCase() === 'true' } diff --git a/apps/sim/tools/brex/update_vendor.ts b/apps/sim/tools/brex/update_vendor.ts index 7c55049b858..713e4db7fc8 100644 --- a/apps/sim/tools/brex/update_vendor.ts +++ b/apps/sim/tools/brex/update_vendor.ts @@ -55,6 +55,11 @@ export const brexUpdateVendorTool: ToolConfig