@go-labs-sg/bb
v2.39.0
Published
Budget Builder CLI for AI agents — manage budgets, bills, claims, quotations, and customer invoices with explicit workflow previews for sensitive changes.
Readme
bb
Command-line interface for Budget Builder. Call budgets, bills, claims, approvals, suppliers, dashboard, and more from your terminal or from tools that don’t support MCP (e.g. some AI agents). Responses are JSON on stdout.
For AI agents: bills and claims are stored as the same underlying bill records. Use the isClaimable column/flag to distinguish them: isClaimable=false means a normal bill, while isClaimable=true means a reimbursable claim.
For AI agents using this CLI: always check whether a newer @go-labs-sg/bb version is available before running Budget Builder commands, then update the CLI if needed so command flags and API behavior stay current.
For AI agents using this CLI: run bb auth whoami before reading or changing Budget Builder data. The CLI displays every command regardless of the authenticated user's role, but the API authorizes each request using the current browser-authorized CLI user or automation API-key owner, resource ownership or project assignment, current workflow state, and approval assignment. See the role-aware agent guide before choosing commands.
For AI agents using this CLI: do not execute mutations until the user explicitly confirms the exact entity, target state, and side effects. Inspect the entity first, summarize what will change and whether emails, external systems, deletions, or financial records are involved, then wait for a clear confirmation from the user. Canonical v2 mutations print an effect-aware workflow preview before one interactive CONFIRM prompt. Non-interactive automation must supply every applicable granular --allow-* flag; the CLI rejects the command before its first API call when any flag is missing.
Registry: @go-labs-sg/bb
Company discovery and reuse
bb company search --query "a star" [--page 2] searches stored BB companies and QuickBooks company customers, including inactive records. Results identify bb, qbo, or both; punctuation/spacing normalization is for suggestions only. Search does not write to either system.
After reviewing a result, bb company use --companyId <bbId> reuses BB and resolves its QBO customer. bb company use --quickbooksId <qboId> imports the existing QBO name/website when BB is missing. Supply both IDs to explicitly connect two unlinked records whose names differ; existing company details are preserved. If QBO has no website, add --website <url> for the BB import only.
The shared API rechecks existing records at submission. Canonical matches reuse records; fuzzy matches require an explicit choice or --confirmDistinct true to confirm a separate legal entity. Inactive companies and conflicting links require review, never implicit reactivation. QBO project/sub-customers are not company-import candidates. Provider failures are not treated as missing records.
bb company use and bb company create are state-changing, potentially external-writing commands. In non-interactive mode their existing permission gates require --allow-state-change --allow-external-write. Deploy the backend before publishing this CLI change. See company discovery details for limits and retry behaviour.
Shared catalogue editing
All active authenticated users can edit suppliers, inventory items and item categories. Normal-user supplier updates submit a revision for approval in the shared API; item/category edits apply immediately. Lead/Admin supplier edits bypass human review. bb supplier update returns the server-owned approval/synchronization result without issuing another approval request. New supplier usage stays on hold during review and linked QuickBooks synchronization; existing usage continues with the last approved details until the edit is approved. Creator identity and delete/archive permissions are unchanged.
See the supplier review and rollout policy. Deploy the backend before publishing this CLI change.
Runtime and installation
The CLI requires Bun 1.4.0 or newer and a Budget Builder account with any active user role.
Check whether a compatible Bun version is already available:
bun --versionIf Bun is missing, install it for the current operating system:
macOS (Homebrew)
brew install oven-sh/bun/bunWindows (PowerShell)
powershell -c "irm bun.sh/install.ps1|iex"Linux
curl -fsSL https://bun.com/install | bashIf Bun is installed but older than 1.4.0, run bun upgrade. Open a new terminal if the installer changes PATH, then verify bun --version again. Installation agents should skip the platform installer when Bun 1.4.0 or newer is already available.
Install and verify the CLI:
bun add --global @go-labs-sg/bb
bb versionOr run without installing:
bunx @go-labs-sg/bb <command>Authentication
Interactive users authorize the CLI through Budget Builder's normal Google-backed browser session:
bb auth login
bb auth status
bb auth whoamiThe browser always requires an explicit Authorize CLI action, even when it is
already signed in. The CLI polls automatically and stores only Budget Builder-issued
credentials through Bun.secrets. It never stores Google credentials. Use
bb auth logout to revoke the interactive session. For SSH or containers, pass
--no-browser and open the printed URL on another device.
Budget Builder API keys remain supported for MCP, CI, and non-interactive
automation. When BB_API_KEY is present it takes precedence over the interactive
session for that process. Create and manage automation keys under API Keys or
with the commands below:
bb user api-key create --name "My automation"
bb user api-key list
bb user api-key revoke <api-key-id>The raw key is returned only by bb user api-key create; copy it immediately.
Never put it in command arguments, source control, or logs. Listing returns metadata
only. Revoking the key currently stored in BB_API_KEY causes that automation
process to fail until the environment credential is replaced or removed so the
interactive session can be used.
Admins can provision API-only service identities and manage their keys from the
CLI. These identities are not human web accounts: create-user does not create
or link a Google OAuth account, so the resulting user cannot sign in with Google.
bb user create --email [email protected] --name "Budget Agent" --role USER
bb user api-key create --userId <user-id> --name "Budget Agent CLI"
bb user api-key list --userId <user-id>
bb user api-key revoke <api-key-id> --userId <user-id>The CLI talks to https://budget-builder.getout.events by default. For staging or local development, the target precedence is --api-url, then BB_API_URL, then production. Overrides must use HTTPS, except http://localhost and http://127.0.0.1 for local development. bb version reports the effective target.
Role-aware agent startup
Every agent session must begin with:
bb version
bb auth whoamiwhoami returns the identity and database role attached to the active browser session or automation key inside the v2 response envelope:
{
"ok": true,
"data": {
"id": "...",
"name": "...",
"email": "...",
"role": "USER"
},
"meta": {
"command": "auth whoami",
"cliVersion": "2.0.0",
"apiBaseUrl": "https://budget-builder.getout.events"
}
}Record that identity in the agent's working context before selecting commands. Do not infer the role from the key name, email address, task description, or commands shown by bb help. Do not switch to another key to bypass a permission failure.
The following is a practical guide, not a client-side allowlist. The API remains authoritative.
| whoami.role | What an agent may normally do | Important limits |
| --- | --- | --- |
| USER | Manage its own API keys; use collaboration endpoints intentionally shared with every authenticated role, including company, contact, and project creation/update; work with resources it created or projects/budgets where its user ID is assigned; create its own bills, claims, and quotation drafts; request approval. | Shared collaboration endpoints are explicit API exceptions, not a general grant over every resource. No admin dashboards, user provisioning, integration recovery, final finance actions, or approval decisions. |
| INSIDE_SALES | All applicable base-user operations, assigned project/budget work, and the broader supplier workflow exposed to Inside Sales. | Budget and supplier approval requests still follow the Lead/Admin workflow. Assignment, ownership, state, and recorded-approver checks still apply. |
| LEAD | All applicable base-user operations plus Lead-stage budget and supplier approval work; supplier creation is auto-approved for this role. | Lead is not Admin. Do not attempt admin dashboards, user administration, integration recovery, final bill payment, or admin-only customer-invoice actions. |
| ACCOUNTING_TEAM | All applicable authenticated operations plus finance-stage bill work, including checking bills and correcting allowed payment or invoice-number metadata. Act on bill approvals only when the API lists the user as an approver. | Direct APPROVED and PAID bill status changes, customer-invoice payment, user administration, and system operations remain Admin-only. |
| ADMIN | Cross-user API-key and service-identity administration, global dashboard/error/integration operations, broad resource access, final bill approval/payment, and admin customer-invoice workflows. | Admin access does not bypass valid state transitions, required evidence, resource consistency, external-operation locks, or the agent's duty to obtain explicit confirmation for sensitive commands. |
Permissions are also resource- and state-dependent:
- Budget/project mutations generally require Admin, resource ownership, or assignment as Business Development, Inside Sales, or Project Manager.
- Bill and quotation edits/deletes are commonly limited to their creator or Admin, and only in supported statuses.
- Budget, supplier, bill, and quotation approval decisions require both a pending approval assigned to the authenticated user and the user's current database role. Budget/supplier decisions require
LEADorADMIN, bill decisions requireACCOUNTING_TEAMorADMIN, and quotation decisions requireADMIN; a stale assignment does not preserve authority after a role change. - Customer-invoice approve/reject commands are Admin batch operations selected by batch ID, rather than approval-record-ID commands. The API maintains and resolves the internal pending Admin approval record as part of the batch workflow.
- Some company, contact, project, supplier, and catalog collaboration procedures intentionally allow every active authenticated role. Treat those endpoint-specific rules as exceptions; do not infer access to adjacent mutations.
- Own-key commands are available to every active role. Passing
--userIdto manage another user's keys requires Admin. bb helpis a command catalog, not proof that the current identity is authorized.
If the API returns FORBIDDEN, stop. Report the command, target resource, whoami identity/role, and the missing ownership, assignment, approver, or role condition when known. Do not retry through a lower-level command or another credential unless the user explicitly changes the operating identity.
Usage
bb help
bb version
bb auth whoami
bb budget list
bb budget get <budget-id>
bb budget status update <budget-id> <status>
bb bill list
bb bill approve <bill-id>
bb approval list
bb supplier listGlobal options and command flags use --key=value or --key value. Run bb help for the canonical command catalog and bb help --legacy for the transition-era detailed flat-command reference.
Run bb version before agent-driven work to record the installed package version and production API target. This is read-only and does not require authentication.
Authoritative command list: run bb help or inspect the generated command-reference.md / command-manifest.json. The catalog includes every canonical route, global option, and effect classification, but does not filter by the current whoami role. During the handler migration, command-specific arguments retain the compatibility dispatcher's validation and are explicitly marked legacy-passthrough in the manifest; use bb help --legacy for their detailed transition reference. Version 2 uses grouped, resource-first commands such as bb budget list and bb bill attachment upload. Flat commands such as bb list-budgets and their MCP-style snake_case aliases remain available during the v2 transition, with a deprecation notice and an equivalent grouped command in help. MCP exposes a subset of the same tRPC surface; the CLI additionally includes a few procedures mainly used by the web UI.
Budget status automation: Setting a budget to ESTIMATE_ACCEPTED requires a confirmed win-proof attachment. --invoiceSettings '<json>' remains available when the project needs saved delivery preferences, but invoice creation chooses its amount independently from those settings. If the parent project is PITCH or LOST, the API marks it WON automatically and runs enabled Asana automation. When rejecting the only accepted/closed budget on a commercial project, pass --projectStatusOnCommercialRejection PITCH|LOST.
Typeform inbound administration
Inbound collection administration requires an API key owned by an ADMIN.
Source and submission lists omit raw Typeform answers; fetching one submission
returns its complete retained payload, which may contain personal information.
bb inbound source list
bb inbound source create --name "Website inbound" --formId "AbCdEfGh" --allow-state-change
bb inbound source get <source-id>
bb inbound source update <source-id> --name "Website inbound" --allow-state-change
bb inbound submission list --sourceId <source-id>
bb inbound submission get <submission-id>
bb inbound mapping save <source-id> \
--payload '[{"canonicalField":"COMPANY_NAME","typeformFieldId":"field-id","typeformFieldRef":"company_name","required":true}]' \
--allow-state-change
bb inbound automation save <source-id> --payload '<automation-config-json>' --allow-state-change
bb inbound processing update <source-id> --mode SHADOW --enabled true --allow-state-changeMapping saves create and activate an immutable version. Automation configuration
stores Slack, Asana, Google Calendar, and the public GOSH brief URL without
secrets, including
the Asana section IDs used by the four Slack stage commands. Processing can
remain in SHADOW to create durable plans without provider writes; use ACTIVE
only after disabling the matching n8n Typeform trigger. These commands do not
enable company, project, or QuickBooks creation.
Use bb inbound source get to inspect discovered Typeform field IDs and the
currently active mapping before saving a replacement version.
New sources are registered against an existing Typeform form ID and always start
with processing disabled in COLLECT_ONLY; creating a source does not call any
external provider.
Set asanaCustomFields.bbLinkFieldId in the automation configuration to the
Asana Deals project's text custom field BB Link. Keep the other configuration
values when saving. Project creation queues a retryable update of that field
with the BB project's /budgets URL; an unmapped field is left untouched.
Older inbound snapshots can use the current BB Link mapping for their original
source and Asana Deals project.
Project Calendar descriptions list bb :, asana deal:, and slack: links
when available. The asana project: line appears only after a win and an
operational Asana project exists. Persisting that Asana project queues a Calendar
refresh, including recovery paths. Inbound response identity is retained for
safe reconciliation. Calendar and configured Asana/Slack synchronization failures
remain retryable in the durable lifecycle outbox.
Mutation safety
While an estimate email is QUEUED, PROCESSING, or awaiting provider
reconciliation, the API makes that Budget read-only. Budget-changing CLI
commands return a conflict until delivery becomes SENT or FAILED.
Every canonical command is classified by effect: state-change, email, external-write, delete, and/or financial-write. Interactive runs require one exact CONFIRM. Non-interactive runs require the corresponding flags (--allow-state-change, --allow-email, --allow-external-write, --allow-delete, and --allow-financial-write); commands with multiple effects require every matching flag. Agents must still get user confirmation in chat first—the runtime gate is not user authorization.
Draft-only bills: Use bb bill create --draft-only --payload '<bill-json>' --allow-state-change --allow-financial-write when the user authorizes a draft only. The equivalent payload field is draftOnly: true. This creates DRAFT for every role, without auto-checking, auto-approval, approval requests, emails, or QuickBooks writes. It cannot be combined with alreadyPaid: true; conflicting flag and payload values are rejected. Draft-only creation uses the dedicated bill.createDraft API route and fails on older servers without falling back to ordinary creation. Deploy backend support before releasing the updated CLI. Reconciled projects must be moved to Won separately before draft-only creation, because reopening a project can trigger external integrations. Completed projects retain their existing creation restriction.
Bill amounts and GST (create and update)
All bill amount fields below exclude GST. This applies to ordinary creation, draft-only creation, and updates. The CLI does not convert a GST-inclusive invoice total into a subtotal. BB calculates applicable GST separately using the supplier's GST registration and each line's out-of-scope setting.
| Payload field | Meaning |
| --- | --- |
| amount | Amount being billed now, before GST. For a deposit or partial bill, use only the portion being billed. |
| extractedAmount | Full supplier invoice amount before GST, as shown on the attachment. For a partial bill, this can exceed amount. |
| lineAmounts[].amount | This bill's allocation to each selected budget item, before GST. The entries must sum to amount. |
| quotationAllocations[].amount | Quotation coverage for each bill line, before GST. Use the same tax basis as lineAmounts. |
For an invoice showing S$1,800 subtotal + S$162 GST = S$1,962 total, a full bill uses the following amount fields (fragment only; include the other required bill fields):
{
"amount": 1800,
"extractedAmount": 1800,
"lineAmounts": [{ "budgetItemId": "item-a", "amount": 1800 }],
"quotationAllocations": [
{ "budgetItemId": "item-a", "quotationId": "quote-a", "amount": 1800 }
]
}Do not put 1962 in either amount or extractedAmount for this full bill: BB would treat it as a before-GST value. A 50% partial bill against the same invoice uses amount: 900, extractedAmount: 1800, and line/quotation allocations totaling 900; with 9% GST applicable to that line, its total is S$981.
Use the invoice's actual subtotal and tax breakdown. Do not blindly divide every invoice total by 1.09: non-GST-registered suppliers, out-of-scope lines, and mixed-tax invoices need their actual breakdown. If the attachment does not establish the before-GST amount, clarify it before creating or updating the bill.
After a mutation, re-read the bill and verify amount, extractedAmount, line amounts, and quotation allocations against the attachment. Report the subtotal, GST, and total separately; do not describe the stored amount as the GST-inclusive total. When correcting a gross/net input mistake, also check extractedAmount so the full invoice is not accidentally represented as a partial bill.
Quotation-first supplier bills: Use quotationIds when approved quotations cover disjoint bill lines. When two quotations fund the same bill line, pass quotationAllocations entries with { budgetItemId, quotationId, amount } and an exact lineAmounts entry for every selected budget item; lineAmounts must sum to the bill amount. Canonical allocation rows are the stored source of every bill-line quotation relationship. A required zero-value bill line needs one or more allocations with amount: 0, which record its approved source(s) without consuming quotation capacity. Every quotation must belong to the bill supplier and project and cover the allocated budget item. Legacy quotationId remains valid as single-quotation input and maps to the current selection. Supplier bills that qualify for an existing exemption must state that intent with --quotation-exempt. Claims share budget allowance but never consume or link quotations, so quotation source fields are rejected for claims.
Every selected bill or claim line must belong to an active budget in ESTIMATE_ACCEPTED or ESTIMATE_CLOSED, including lines drawn from additional budgets in the project. The server checks this during selection, creation, editing, and approval. An unaccepted budget never qualifies a line for a quotation exemption. Existing pending bills expose budget and source-link blockers in commitmentReview.blockers.
Admins can repair missing source links on eligible draft, pending-approval, or rejected supplier bills with bb bill quotation link <billId> --quotationIds q1,q2 --allow-state-change --allow-financial-write. For same-line splits, use --payload '{"quotationAllocations":[{"budgetItemId":"line_1","quotationId":"quote_1","amount":400},{"budgetItemId":"line_1","quotationId":"quote_2","amount":350}]}'. The repair uses stored bill-line amounts and changes source linkage only.
bb bill create --payload '{"projectId":"project_1","budgetId":"budget_1","supplierId":"supplier_1","budgetItemIds":["line_1","line_2"],"quotationIds":["quote_1","quote_2"],"amount":1200,"isClaimable":false}'
bb bill create --payload '{"projectId":"project_1","budgetId":"budget_1","supplierId":"supplier_1","budgetItemIds":["line_1"],"amount":750,"lineAmounts":[{"budgetItemId":"line_1","amount":750}],"quotationAllocations":[{"budgetItemId":"line_1","quotationId":"quote_1","amount":400},{"budgetItemId":"line_1","quotationId":"quote_2","amount":350}],"isClaimable":false}'Bill creation determines its required permissions from the parsed input, for both canonical and legacy commands. Ordinary unpaid creation requires state-change, financial-write, and external-write permissions, but no email permission. Already-paid creation also requires email permission. Without draft-only mode, existing role behavior remains: ordinary users create drafts, Accounting creates checked bills, and Admin creates approved bills and writes to QuickBooks. Ordinary creation can also adjust existing QuickBooks placeholder bills. Explicit drafts defer those adjustments until the later approval/QuickBooks workflow. When later authorized to submit the draft, use bb bill approval create <billId> with its state-change and email permissions; Admin-created drafts also enter finance review through this command. The static command catalog lists the union of possible effects.
Approval email exclusions: Approval requests create pending database records for every eligible approver, including configured non-recipient admin accounts. Automated approval-request emails skip those accounts.
Locked estimate budget changes: Legacy flat commands retain their additional interactive yes prompt when the current budget is locked. Canonical v2 commands use the single effect-aware confirmation gate; non-interactive execution requires --allow-state-change plus any other effects declared for that command.
Mutations with --payload: Commands such as bb budget create, bb bill create, and bb supplier update take a single JSON object (--payload '<json>') matching the corresponding tRPC procedure input. Use ISO strings for date/datetime fields; the CLI coerces them where needed. The API still validates the full shape. Plain description fields for item create/update are converted to descriptionRichText; pass descriptionRichText directly when formatted Tiptap JSON is required. For bb project update, the project window is dateRange.from and dateRange.to (optional end); there are no separate event-date fields on the project payload. bb budget create / bb budget update do not accept asanaTaskId; configure the deal card on the project (bb project update / project settings). When bb budget create clones a sourceBudgetId containing unavailable catalogue lines, the API preserves those snapshots for review and rejects the clone until the caller explicitly sets unavailableItemReviewAcknowledged: true.
Unavailable catalogue lines: Budget Builder-only bb item archive --ids <csv> may retire catalogue items referenced by open estimates. The estimate snapshots remain intact, but sending or accepting is blocked until each unavailable line is resolved with bb budget item replace --budgetItemId <id> --replacementItemId <active-item-id>, bb budget item remove <id>, or the Lead/Admin-only bb budget item exception approve --budgetItemId <id> --reason <text>. The approved-exception reason is audited. bb item delete <id> also attempts QuickBooks deactivation and therefore remains blocked while an open estimate references the item.
Contact-person estimate email: bb budget estimate send --payload '<json>' calls the same email.sendEstimateToContactPerson procedure as the web composer. The payload requires budgetId, estimateId, optional estimateDocNumber, to, cc, replyTo, subject, HTML content, and HTML signature. It durably queues worker delivery of the QuickBooks PDF, standard terms, and Budget Builder budget attachments, and returns the outbound-email and operation IDs immediately. Worker retries reuse one provider idempotency key; the budget becomes ESTIMATE_SENT only after Resend accepts the email. The command requires interactive CONFIRM; inspect the budget, recipients, and HTML first.
Invoice amount, due date, and approval
After accepting an estimate, choose any eligible budget with an uninvoiced balance. Create one invoice at a time for all or part of the remaining estimate percentage. Creation is immediate; the due date is the payment deadline.
- Run
bb customer-invoice eligible-budget list <projectId>to review each estimate total, already invoiced percentage, remaining percentage, and available amount. - Run
bb customer-invoice readiness check <budgetId>for live QuickBooks validation and the full remaining-balance preview. - For a partial invoice, run
bb customer-invoice amount preview <budgetId> <percentage-of-estimate>to review the amount before GST, GST, and total. The percentage is based on the original estimate and cannot exceed the remaining percentage. - Run
bb customer-invoice create --payload '<json>'with a recommended UUID request ID, invoice date, billing address, customer memo, and exactly one split containing its estimate percentage and due date. The CLI repeats live readiness before asking for confirmation; blocked invoices report the readiness issue without writing to QuickBooks. The server checks readiness again before creation. - Non-admin creation automatically requests admin approval after QBO creation. Admin creation is auto-approved. Pending approval never sends invoices.
- Use the normal invoice detail/email or portal-upload actions after approval. Creation and approval do not automatically email the customer.
Example for a 50% invoice:
{
"budgetId": "budget_1",
"requestId": "6ec0d99f-24a4-4f3e-9a74-d3f4fd971a25",
"invoiceDate": "2026-11-21",
"details": {
"billingAddress": "Customer name\n1 Example Street\nSingapore 123456",
"customerMemo": "Thank you for your business."
},
"isCustomPercentage": true,
"splits": [
{ "label": "Partial invoice", "percentage": 50, "dueDate": "2026-12-01" }
]
}Use the exact percentage returned by the preview flow. Non-interactive creation requires --allow-state-change --allow-financial-write --allow-external-write --allow-email (internal approval notification, not customer delivery). Reuse the same requestId when retrying an uncertain request, and review the existing batch before retrying.
To link an invoice that already exists in QuickBooks, first run bb customer-invoice existing list <budgetId>. Choose a returned qboInvoiceId, review it with bb customer-invoice preview-qbo <budgetId> <qboInvoiceId>, then use the preview token with bb customer-invoice import-qbo <budgetId> <qboInvoiceId> --token <previewToken>.
The former splits, preview-splits, save-splits, create-splits, and send-group schedule commands are retired. Update the CLI alongside the API. Existing schedule data and invoices are retained, and previously queued emails retain their delivery/reconciliation support; no new schedules or reminders are created.
Quotation amounts and line allocations
Quotation create and update payloads use amountWithoutGst for the supplier subtotal and gstAmount for GST. Keep GST separate; do not add it to amountWithoutGst. When a quotation covers more than one budget item, lineAmounts is required and must contain each selected item exactly once. The line amounts must sum to amountWithoutGst.
Use the actual budgetItemIds returned by bb budget get <budget-id> or the relevant budget-item read. Do not invent IDs, use item names, or reuse IDs from another project or budget. quotationAllocations belongs to bill creation/update as the bill's source-quotation allocation; it is not a quotation create/update field.
Upload exactly one quotation document first. Pass the single attachment object returned by bb quotation attachment upload <project-id> <file-path> in the quotation payload's one-element attachments array. The server accepts one original quotation file and normalizes it to PDF when needed.
Replace the example attachment size 12345 with the actual byte count from the upload response.
For a quotation split across two real budget lines at S$750 and S$450, with no GST, the complete CLI payload shape is:
bb quotation attachment upload "<project-id>" "<quotation-file.pdf>"
# Replace every <...> placeholder with the matching value from the reads and
# the upload response. The attachment array must contain that one upload object.
bb quotation create --payload '{
"projectId": "<project-id>",
"budgetId": "<budget-id>",
"supplierId": "<supplier-id>",
"budgetItemIds": ["<budget-item-id-for-750>", "<budget-item-id-for-450>"],
"lineAmounts": [
{"budgetItemId": "<budget-item-id-for-750>", "amount": 750},
{"budgetItemId": "<budget-item-id-for-450>", "amount": 450}
],
"amountWithoutGst": 1200,
"gstAmount": 0,
"attachments": [
{
"id": "<attachment-id-from-upload>",
"name": "<attachment-name-from-upload>",
"key": "<attachment-key-from-upload>",
"size": 12345
}
]
}'Use the same fields for bb quotation update --payload, adding the existing quotation "id": "<quotation-id>". Read the quotation and its budget before updating so the IDs and current status are confirmed. The example's subtotal is S$1,200, GST is S$0, and total is S$1,200; those values are illustrative only and contain no live resource IDs.
Link an existing QuickBooks invoice
Release status: these CLI commands are pending release. The repository package version remains 2.31.0; that version number alone does not establish that a published CLI contains them. Check bb help for customer-invoice preview-qbo and customer-invoice import-qbo after the normal CLI release. This code delivery does not publish the CLI, deploy the API, or perform a production import. The backend must expose customerInvoice.previewExistingInvoice and customerInvoice.linkExistingInvoice (implemented in PR #1067).
Authenticate and run bb auth whoami first. Preview and import use the same project management permission as the web flow: with user access checks enabled, Admins or the project's creator, business-development owner, inside-sales owner, or project manager can act. A finance/Lead role alone does not grant access to an unrelated project. The server applies its current access-check configuration and rechecks permission during confirmation.
Use the BB budget ID whose existing estimate should receive the invoice, and the numeric QBO invoice transaction ID, not the displayed invoice number or an estimate number. Accepted and closed estimates are supported; linking does not reopen them. Fully invoiced means all estimate coverage has been billed; it does not mean the invoices are fully paid.
bb customer-invoice preview-qbo <budgetId> <qboInvoiceId>
bb customer-invoice import-qbo <budgetId> <qboInvoiceId> --token <previewToken>Preview performs read-only QBO access and returns JSON under data, including token, project/estimate, invoice number/identity, currency, dates, total, outstanding balance, allocation percentage, lines, alreadyLinked, and other unlinked invoices. Review all of it before confirming. Import requires the exact data.token from that preview; it does not silently generate a fresh token. In an interactive terminal, confirm the named invoice and budget when prompted. For an explicitly authorized non-interactive import:
bb customer-invoice import-qbo <budgetId> <qboInvoiceId> \
--token <previewToken> --allow-state-change --allow-financial-writeImport writes BB financial records only and needs neither --allow-external-write nor --allow-email. It calls the same server validation as the UI: project/customer/currency/estimate/allocation checks, sibling invoice coverage, and financial drift checks. It preserves the original invoice evidence and does not create, update, void, delete, send, or match anything in QBO. A successful response returns batchId, invoiceId (the new BB invoice ID), and alreadyLinked: false. Repeating the same link returns the original IDs with alreadyLinked: true, provided upstream financial evidence still agrees. A conflicting budget, ambiguous allocation, or financial mismatch fails; a stale preview requires another reviewed preview. Do not retry a conflict with a different budget to bypass it.
To refresh payments, use the returned BB invoice ID:
bb customer-invoice sync <invoiceId>For QBO_IMPORT invoices, this existing command reads QBO and updates BB balance, payment status, and sync evidence only. Settlement becomes PAID; a reversal restores APPROVED; original invoice evidence is retained. Financial drift is reported separately as qboFinancialDrift and blocks further billing. The shared sync command also supports native invoices, so its generic automation permissions remain --allow-state-change --allow-financial-write --allow-external-write; imported records still take the server's BB-only path. Native creation continues to use bb customer-invoice create --payload '<json>' and needs no import token or fields.
The Seatrium example must be previewed against live QBO again before an operator imports it. Previously observed invoice numbers and paid/open balances are not authorization or current-state evidence.
Customer-invoice workflow parity: bb customer-invoice list uses the same global/project list procedure and metrics as the web pages; omit filters for the global list or use --projectId for project scope. bb customer-invoice send --payload '<json>' uses the same protected email workflow as the web composer, sends to the payload's to address, saves that address as the project's configured billing email after a successful send, and marks the invoice SENT. bb customer-invoice approve and bb customer-invoice reject are Admin-only batch operations: callers select a batch ID, and the API resolves its internal pending Admin approval record. bb customer-invoice sync follows QuickBooks' paid state and zero balance, restoring SENT or APPROVED if that payment is reversed. Invoice approval refuses voided invoices and closes the estimate only when approved invoice coverage totals 100%; deletion, voiding, rejection, expiry, and QBO synchronization use the same estimate-reopening and live-payment guards as the web app.
For agent-driven invoice work, use this read-before-write sequence:
- Run
bb customer-invoice get <batchId>and, when composing email,bb customer-invoice email-context get <batchId>. - For creation, run
bb customer-invoice eligible-budget list <projectId>orbb customer-invoice readiness check <budgetId>first. - State the exact invoice or batch, current status, intended mutation, QuickBooks effect, email recipients, and estimate-closing/reopening effect to the user.
- Wait for explicit confirmation, then run the sensitive command interactively and verify its JSON result. Do not treat the runtime
CONFIRMprompt as user authorization.
Project Hub operations
Project Hub setup and commercial-document reconciliation are durable, asynchronous Budget Builder operations. Read status before requesting either mutation:
bb project hub list --q <project-or-company> --status WON,COMPLETED
bb project hub status <project-id>
bb project hub setup <project-id>
bb project hub sync <project-id>list is read-only and returns active projects with a Project Hub integration, including setup state and the stored Hub URL when available. It supports project/company search, project-status filters, pagination, and sorting.
status reports eligibility, whether setup can be requested, the current setup status, the latest setup error, and the client Hub URL. setup uses the same eligibility and accepted-estimate checks as the web button and may create the operational Asana project before provisioning Drive and Project Hub. sync requires an existing READY native-layout integration and queues a complete reconciliation of accepted estimates, commercial attachments, win proofs, and eligible customer invoices into Asana and Drive.
Both mutation commands write external state asynchronously and therefore require state-change and external-write confirmation. In non-interactive use, pass both --allow-state-change and --allow-external-write only after obtaining explicit user approval. A successful queued response means the durable operation was accepted. status tracks setup readiness; the sync response returns its outbox operation ID, which an Admin can inspect with the integration-operation commands when completion or failure diagnostics are required.
The fleet-wide legacy-layout migration is intentionally not exposed through the normal CLI. It remains a restricted deployment script that defaults to a read-only dry run.
Command overview
The table below is a behavior index keyed by the compatibility dispatcher's legacy handler labels. It is not invocation syntax. AI agents and new automation must resolve and use the grouped v2 route from bb help, command-reference.md, or command-manifest.json; never copy a flat label from this table into a new command.
Project and budget query payloads keep the Asana identities explicit: asanaTaskId is the Deals task GID, while asanaProjectGid and asanaProjectUrl identify the operational project created for Won delivery work.
Bill, claim, and quotation mutations accept only suppliers whose approval status is APPROVED. An active supplier that is PENDING_APPROVAL, REJECTED, or SUPERSEDED must be approved before it can be used.
| Area | Legacy handler labels (reference only; non-exhaustive) |
| --- | --- |
| Budgets | list-budgets (full payload by default; --summary or --includeDetails false for slim list), get-budget, get-budget-items, get-budget-details, get-budget-categories, get-budget-versions, rename-budget-version, restore-budget-version, update-budget-status (ESTIMATE_ACCEPTED requires win proof, auto-marks PITCH or LOST projects WON, and auto-creates an Asana Event Ops section from Prompt 5 tasks when enabled; Prompt 5 skips quantity-zero items, combines matching non-GO-internal suppliers, keeps each GO internal item separate, removes generated work that repeats another line item or group, and schedules applicable subtasks relative to the project start date; --projectStatusOnCommercialRejection PITCH\|LOST is required when rejecting the only accepted/closed budget on a commercial project), mark-budget-won (<budgetId> + proof file path; PITCH or LOST projects become WON automatically), create-budget / update-budget (--payload; cloned unavailable lines require unavailableItemReviewAcknowledged: true), delete-budget, create-budget-approval (also sends approval request emails), create-estimate, send-estimate-to-contact-person (same contact-person email workflow as web), add-budget-items, update-budget-item, replace-budget-item, approve-unavailable-item-exception (Lead/Admin), remove-budget-item, reorder-budget-items, update-budget-item-supplier, mark-budget-items-not-utilized, restore-budget-item, create-placeholder-bill (admin recovery), create-budget-category, update-budget-category, delete-budget-category, update-budget-commission, delete-budget-commission, update-budget-discount, delete-budget-discount (--payload where noted), upload-budget-attachment (<budgetId> + local file path; uses attachment.requestBudgetAttachmentUpload + PUT + attachment.confirmBudgetAttachment) |
| Bills / claims | list-bills (--isClaimable false for bills, --isClaimable true for claims, omit for both), list-claims (claims only), validate-bill-selection (--payload, plus --quotationIds q1,q2 or legacy --quotationId q1; validates the approved supplier and quotation coverage), link-bill-quotations (<billId> --quotationIds q1,q2; Admin source-link repair for eligible supplier bills), stage-bill-attachment (securely uploads invoice/payment-proof files before creation and returns attachment JSON; ownership and one-hour expiry are enforced by a server-side staged-upload record rather than encoded in the object key), cleanup-staged-bill-attachments, create-bill (--payload; set isClaimable=false for a bill and provide approved quotationIds, or use --quotation-exempt for an eligible server-validated exemption; set isClaimable=true for a claim without quotations; supplier bills require positive before-GST extractedAmount (full invoice subtotal) and amount (portion billed), with amount <= extractedAmount; line and quotation allocation amounts also exclude GST; an already-paid supplier bill sets alreadyPaid=true and requires paymentReference plus a staged PDF in paymentProofAttachments; admin creation automatically queues QBO finalization while other roles remain pending approval), update-bill (--payload), update-bill-payment-evidence (--payload; replaces the payment reference and payment-proof PDFs for an already-paid bill), delete-bill, create-bill-approval (also sends approval request emails), update-bill-status (PAID requires --paymentReference; pass --paymentProof <receipt.pdf> to stage and submit a PDF up to 20MB atomically, or omit it only when BB already has payment proof; moving to PAID runs the server's paid-bill notification workflow), patch-bill-payment (PAID bills: --paymentTrackingUrl, --paymentReference, --quickbooksBillId, --paymentDate ISO; clearing a paid bill's QuickBooks link is not allowed), patch-bill-invoice-number, get-bill-attachments, upload-bill-attachment (<billId> + local path), get-bill-details |
| Quotations | list-quotations (supports project, budget, supplier, status, requester, and text-search filters), get-quotation, upload-quotation-attachment (<projectId> + PDF/JPEG/PNG path up to 20MB; returns attachment JSON for payload use), cleanup-staged-quotation-attachments, create-quotation (--payload for quotation.createDraft; include amountWithoutGst, gstAmount, and lineAmounts entries { budgetItemId, amount } for multiple selected items), update-quotation (--payload; updates a DRAFT or REJECTED quotation), delete-quotation, submit-quotation, approve-quotation, reject-quotation, download-quotation-pdf (original, staff, or final) |
| Customer invoices | check-customer-invoice-readiness, list-eligible-customer-invoice-budgets, preview-customer-invoice-amount, list-available-qbo-customer-invoices, list-customer-invoices (global/project/budget filters plus summary metrics; --sortBy totalInvoiceAmount sorts provider-confirmed totals including GST, with unknown amounts last), get-customer-invoice, get-customer-invoice-email-context, preview-qbo-customer-invoice, import-qbo-customer-invoice (BB-only link with reviewed token), create-customer-invoice, discard-customer-invoice, delete-customer-invoice, void-customer-invoice, approve-customer-invoice (admin), reject-customer-invoice (admin), send-customer-invoice-to-contact-person, download-customer-invoice-pdf (non-admins cannot download while approval is pending), sync-customer-invoice |
| Approvals | list-approvals / get-pending-approvals (--type budget\|supplier\|bill\|quotation\|customer_invoice\|all), approve-bill / reject-bill (send reply email), approve-budget / reject-budget (send reply email), approve-supplier / reject-supplier (send reply email), approve-quotation / reject-quotation, approve-customer-invoice / reject-customer-invoice |
| Companies & projects | list-companies, get-company, create-company, update-company (--payload), delete-company, list-projects, get-project (its budget overview returns totalRevenue, calculated only from Estimate Accepted and Estimate Closed budgets), get-project-hub-status, setup-project-hub, sync-project-hub-commercial-documents, create-project (required: --name, --companyId, --contactPersonId, --insideSalesId, --businessDevelopmentId, --venue, --startDate as ISO datetime for project/window start; when --asanaTaskId is omitted, the CLI searches open Asana lead tasks in the Deals project, prompts for one of the top five matches, and resolves Slack channel fields from the selected deal card; optional --asanaSearch, --pax, --endDate, --description, --projectManagerId; always requests QBO project import like the web app), update-project (--payload with dateRange.from / dateRange.to for the project window; optional projectManagerId and requestQboAccountantNotification in JSON), delete-project, check-project-reconciliation <id> (runs the web app's live checks without changing status), reconcile-project <id> (reruns checks transactionally and marks an eligible project RECONCILED), complete-project <id> (reruns validation, marks a reconciled project COMPLETED, and queues QuickBooks placeholder cleanup), import-qbo-project, update-project-status (<id> <status>: PITCH | WON | COMPLETED | RECONCILED | LOST; close-out follows WON → RECONCILED → COMPLETED, and reconciliation requires every accepted/closed budget line to have an Approved/Paid bill or claim or be explicitly Not Utilized; marking COMPLETED queues deletion of every remaining project placeholder bill from QuickBooks; for PITCH → WON also pass a Budget Builder user --projectManagerId or --projectManagerEmail; when marking WON without an accepted/closed budget or proof, pass --wonOverrideReason) |
| Contacts | list-contacts, create-contact-person (--payload), update-contact-person (--payload) |
| Suppliers & items | list-suppliers (defaults to active suppliers, --perPage 10, sorted by createdAt desc; supports --name, --sortBy for scalar supplier fields, --sortDir, --createdBy, --gstRegistered, --status, --supplierTags, --active false for archived suppliers), create-supplier (--payload; when supplier status is PENDING_APPROVAL, also runs supplier.createSupplierApproval and email.sendSupplierApprovalRequestEmail), update-supplier (--payload; when supplier status is PENDING_APPROVAL, also runs supplier.createSupplierApproval and email.sendSupplierApprovalRequestEmail), delete-suppliers (--ids CSV; admin; archives/deactivates related items), reactivate-suppliers (--ids CSV; admin; reactivates related items), create-certification / create-payment-method / create-supplier-role / create-supplier-tag (--name), get-supplier-details (includes supplierApprovalSummary for pending approvers, superseded approvers, and the actual responder/respondedAt metadata), get-supplier-analytics (admin), list-items (--includeQuickBooksDeactivationPending true discovers interrupted QuickBooks deactivations for Admin retry), create-item (--payload), update-item (--payload), archive-items (--ids CSV, max 100; Budget Builder-only; open estimates retain unavailable snapshot lines for resolution), delete-item (admin; archives in Budget Builder and makes the item inactive in QuickBooks; blocked while an open estimate references it), get-item, list-item-categories, create-item-category / update-item-category / delete-item-categories (admin; --ids CSV for delete) |
| Dashboard & users | whoami (current authenticated identity and role), list-users, create-user (admin; provisions an API-only service identity with no Google sign-in; --email, optional --name, optional --role defaulting to USER), create-api-key (--name; defaults to the caller; admin-only --userId for another user; raw key shown once), list-api-keys (defaults to the caller; admin-only --userId for another user), revoke-api-key (key ID; admin-only --userId for another user), get-user-performance, get-dashboard, get-monthly-metrics, get-system-overview, get-estimate-performance, get-financial-overview (performance/dashboard commands are admin-only) |
| Errors (admin) | get-recent-errors, get-error-metrics |
| Automation (admin) | list-integration-operations, retry-integration-operation (Resend resolution requires --confirmExternalStateReconciled true plus --outboundEmailResolution ACCEPTED\|NOT_ACCEPTED_RETRY; include --providerMessageId when accepted) |
| Historical / benchmarks | get-approved-budgets, get-budget-category-benchmarks, get-item-pricing-history, get-supplier-pricing-history |
The CLI intentionally wraps low-level upload-request/confirm procedures into file-based commands and omits browser-only helpers such as navigation counts, combobox/facet data, recent-page bookkeeping, and live UI subscriptions. Operational workflows for bills, claims, quotations, customer invoices, project reconciliation/completion, and integration recovery are available directly.
Output
- Canonical stdout: a pretty-printed
{ "ok": true, "data": ..., "meta": ... }JSON envelope - Canonical stderr: a structured
{ "ok": false, "error": { "code", "message", ... }, "meta": ... }JSON envelope on failure - Diagnostics: off by default for canonical commands; use
--debugfor sanitized stderr tracing and--quiet,-q, orBB_CLI_QUIET=1to suppress diagnostics - Legacy compatibility: flat aliases keep their raw success JSON,
{ "error": "..." }failures, and default action tracing during the transition - Exit codes:
0success,2usage/validation/confirmation failure,1API/auth/network/internal failure,130interruption
A .env file in the current working directory is loaded automatically for automation settings such as BB_API_KEY. Interactive browser credentials remain exclusively in Bun.secrets.
Developing in this repo
From the monorepo root (after bun install):
export BB_API_KEY=...
bun run packages/budget-builder/cli/src/index.ts budget listOr from packages/budget-builder/cli:
bun run build # emit dist/ via tsc
bun run typecheck # tsc --noEmit
bun run src/index.ts budget listLayout
src/prisma-enums.ts— Prisma enum string values for runtime; mirrorpackages/budget-builder/db/src/generated/prisma/enums.tsafter schema changes.src/filter-enums.ts— “extended” filter enums (ALL + Prisma enums). Keep in line withpackages/utils/src/filter-enums.tswhen those values change.
Publish
Build the package first, then create its publishable archive with Bun:
bun run build
bun run pack:artifactpack:artifact copies only the publish allowlist into a temporary staging directory and writes a sanitized publish manifest there. It never edits the source package.json, and the staged archive contains no workspace-only development dependencies. The release workflow packs once, validates the resulting archive, smoke-tests that exact tarball under Bun 1.4, then publishes that exact tarball. Types from @go-labs/budget-builder-api are compile-time only (import type). Prisma enum values used at runtime live in src/prisma-enums.ts (kept in sync with packages/budget-builder/db generated enums) so npm installs do not need @go-labs/budget-builder-db.
The generated command-reference.md and command-manifest.json are release artifacts. Regenerate them whenever the command registry changes; the release workflow rejects a tarball that does not include both files.
Customer invoice lifecycle filters
bb customer-invoice list --status SENT filters by the displayed invoice lifecycle.
The list also accepts PARTIALLY_SENT, PARTIALLY_PAID, PAID, and VOIDED,
alongside existing approval states such as APPROVED and PENDING_APPROVAL.
--sortBy status sorts by lifecycle; lifecycleStatus in list/detail output is
separate from the stored approval/operational status.
Shared allowance decisions
Quotation approval/submission and bill approval/status commands accept an optional
--overBudgetOverrideReason "<reason>". Bill create/update JSON payloads accept
overBudgetOverrideReason. Only admins may override an assessed overrun; missing
relationships or allocations block approval and require review. Multi-line quotation
create/update payloads must include lineAmounts with { budgetItemId, amount }
for every selected line, summing to amountWithoutGst. The server revalidates
existing quotation links, reviewed usage, and per-line allowance inside the
approval transaction. Multiple source quotations use canonical bill quotation
allocation rows; apply the updated database schema and deploy the API before
releasing the CLI. Unresolved historical overlaps continue to block approval.
