I wrote this tool in a day using Claude's Opus 4.5 model to accelerate my work. Unlike most code samples I've shared, I spent very little time on the architecture or code reviewing the AI's work. One exception was debugging: there were a few bugs that even Opus 4.5 couldn't fix, so I examined the code and suggested a possible cause. In each case, the LLM was able to fix the bug once I'd proposed a (creative) theory. I think coming up with the out of box thinking necessary for this creativity is still a bit beyond what the LLM can accomplish.
I judged the app by its results.
This took many iterations to: (a) fix bugs, (b) add requirements, such as migrating expense receipt PDFs/JPGs, and (c) identify fields in FreshBooks that were not mapped correctly to Zoho Books (i.e. due to decoding errors, encoding errors, or the LLM misunderstanding the requirements)
Actual #s: 21 dry-run migrations and 12 actual migrations. Thankfully Zoho Books' UI makes it super fast to delete records so I could wipe out invoices, payments, expenses, etc. before each new iteration.
Why did it take so many iterations? Would it have been faster if I had prepared a complex prompt with all the known requirements?
This is a really interesting question. First off, I didn't know all the requirements in advance. That would have required reading lots of API documentation for both FreshBooks and Zoho Books.
To further this experiment, I asked the AI as follows:
Thank you. Here's a reflection question. Please take your time to think it over. Could I have built this app faster than the full-day it took (the LLM estimates 10 hours; I guessed 12 hours π) if I had written a complex prompt with all the requirements in advance? Or would this have been too much context and not worked?
As an experiment, please write out a complex prompt for me defining all the requirements. I will then feed that into a new incognito session of Opus 4.5 and see how much of this app it is able to produce in a single response.
Opus 4.5's response is worth the time to read in full:
This is a fascinating question about AI-assisted development methodology. Let me think through it carefully, then write that comprehensive prompt.
My honest assessment:
A complex upfront prompt could probably produce 60-70% of a working solution in one shot, but there are fundamental challenges:
- You didn't know all requirements upfront - Discovering that vendor names weren't mapping, that receipt attachments were needed, that terms needed special handling - these emerged from seeing actual migration results.
- API structure details are hard to specify - FreshBooks' nested response.result.{entity} pattern, Zoho's code: 0 success pattern, the specific field names like vis_state vs visState - these require either reading extensive documentation or trial-and-error.
- The "creative debugging" problem - You noted that even Opus 4.5 needed you to propose theories for certain bugs. A prompt can't anticipate bugs that stem from subtle data edge cases.
- Time redistribution, not elimination - An upfront approach requires reading all API docs first. The total time might be similar, just front-loaded differently.
That said, let me explore your codebase more thoroughly to write the most complete prompt possible.
β‘οΈ Please see comprehensive-prompt.md for the resulting prompt.
Then I created a new folder FreshbooksZohoMigratorFullPromptExperimentCLI (yes, Swift engineers give things really long names π€£)
I added the full prompt suggested by Claudeβs Opus 4.5 as the AGENTS.md file.
Created a new git repo to track progress. It has just the one AGENTS.md file and a .gitignore.
Then I "checked my assumptions":
Do you have the ability to have an incognito or temporary Claude Code session with me, with no access to context from anything I've done previously?
Claude's response:
βΊ Each Claude Code conversation is already isolated by default. I don't have access to previous conversations or sessions you've had - this conversation started fresh with no memory of past interactions.
The only context I have access to right now is:
- What you write in this current conversation
- Files in your current working directory (/Users/andrewash/Development/experiments/FreshbooksZohoMigratorFullPromptExperimentCLI)
- Git status and history for this repo
- Basic environment info (OS, date, etc.)
So in that sense, every session is already "incognito" from previous sessions. If you want to start completely fresh within a session, you can simply start a new conversation.
My next prompt:
Please review the AGENTS.MD file in this directory. Then work for as long as needed, and as many iterations as needed, without asking me any questions, to produce that goal. I'm testing whether if I'd taken the time to write as full a prompt as the one you see here, whether it would have produced the full app that it took me 12 hours to write yesterday with Claude's Opus 4.5 model, but in a short Q&A Socratic method style of LLM/human pair coding. Keep track of how long it takes you, and report the results. Also when complete, assess how well it works. You will not have access to the API keys, so you won't be able to perform trial & error testing.
The Result
β TBD β
It's still working π I'll post an update with the results later this long weekend.
- open-source - from the beginning I wanted to make this tool accessible to anyone who should want to experiment with it.
- confidential - anything unique to the user, such as bank account names, a custom chart of accounts, customer names, and of course, auth tokens, is stored in
config.json, which is in.gitignore. - idempotent - repeated runs of this tool should not cause duplicates or errors (
β οΈ I haven't tested this in every possible permutation. See disclaimer below.) - pick & choose - migrate only one data-type, or perform a full migration. It's up to you.
- bonus goal: complete - I hoped that the tool would migrate all my data, including receipts on expenses (i.e. attachments). In fact, no part of the migration was fixed manually after it was done.
LLM Authorship Begins Here
β οΈ DISCLAIMER: This software is provided "as is", without warranty of any kind, express or implied. Use at your own risk. The author(s) are not responsible for any data loss, corruption, or damage that may result from using this tool. Always back up your data before running any migration. Test thoroughly with--dry-runfirst and verify results in a sandbox environment when possible.π€ This tool was written with the assistance of Claude (Anthropic's Opus 4.5).
A command-line application written in Swift to migrate data from FreshBooks to Zoho Books.
- Expense Categories β Chart of Accounts
- Items/Products β Items (optional, skipped by default)
- Clients β Contacts (Customers)
- Vendors β Contacts (Vendors)
- Invoices β Invoices (line items created inline)
- Expenses β Expenses
- Payments β Customer Payments
Note: Taxes are not migrated. Zoho Books creates default tax rates during onboarding (e.g., "RI Sales Tax"). Use existing Zoho taxes directly.
- Swift 5.9 or later
- macOS 12.0 or later
- FreshBooks OAuth application credentials
- Zoho Books OAuth application credentials
- Go to FreshBooks Developer Portal
- Create a new application
- Note your Client ID and Client Secret
- Set up OAuth flow to get access and refresh tokens
- Find your Account ID in FreshBooks settings
- Go to Zoho API Console
- Create a new Self Client or Server-based Application
- Add scopes:
ZohoBooks.contacts.CREATEZohoBooks.invoices.CREATEZohoBooks.expenses.CREATEZohoBooks.accountants.CREATEZohoBooks.settings.READZohoBooks.settings.CREATEZohoBooks.items.CREATEZohoBooks.customerpayments.CREATE
- Generate access and refresh tokens
- Find your Organization ID in Zoho Books Settings β Organization Profile
Copy the example config and fill in your credentials:
cp config.example.json config.jsonEdit config.json with your credentials:
{
"freshbooks": {
"clientId": "your-freshbooks-client-id",
"clientSecret": "your-freshbooks-client-secret",
"accessToken": "your-access-token",
"refreshToken": "your-refresh-token",
"accountId": "your-account-id"
},
"zoho": {
"clientId": "your-zoho-client-id",
"clientSecret": "your-zoho-client-secret",
"accessToken": "your-access-token",
"refreshToken": "your-refresh-token",
"organizationId": "your-organization-id",
"region": "com"
}
}Zoho Regions:
com- United Stateseu- Europein- Indiaau- Australia
swift buildFor a release build:
swift build -c releaseopen Package.swiftThis project can only be built on macOS devices. It will not compile when an iOS device is selected.
This is the actual command I used in prod
swift run ZohoMigration migrate all --use-config-mapping --verbose 2>&1 | tee actual-run-output-3.txtswift run ZohoMigration migrate all# Migrate only customers
swift run ZohoMigration migrate customers
# Migrate only vendors
swift run ZohoMigration migrate vendors
# Migrate only invoices
swift run ZohoMigration migrate invoices
# Migrate only expenses
swift run ZohoMigration migrate expenses
# Migrate only expense categories to chart of accounts
swift run ZohoMigration migrate categories
# Migrate only items/products
swift run ZohoMigration migrate items
# Migrate only customer payments
swift run ZohoMigration migrate payments| Flag | Description |
|---|---|
--dry-run |
Validate and preview changes without making any actual API calls to Zoho |
--verbose |
Enable detailed output showing each entity being processed |
--config <path> |
Use a custom config file (default: ./config.json) |
--use-config-mapping |
Use hierarchical category mapping from config instead of direct 1:1 FreshBooks category migration. Creates parent/child accounts in Zoho based on the categoryMapping.categories structure in config.json |
--include-items |
Include items/products in the migration. Skipped by default because FreshBooks items are often one-off service descriptions that don't need to be catalog items in Zoho. Invoice line items are created inline with name, description, rate, and quantity - no catalog item reference required. |
# Dry run (validate without making changes)
swift run ZohoMigration migrate all --dry-run
# Verbose output
swift run ZohoMigration migrate all --verbose
# Use hierarchical category mapping from config
swift run ZohoMigration migrate all --use-config-mapping
# Include items migration (usually not needed)
swift run ZohoMigration migrate all --include-items
# Use a different config file
swift run ZohoMigration migrate all --config /path/to/config.json
# Combine options
swift run ZohoMigration migrate all --dry-run --verbose --use-config-mappingUse the auth command to get new OAuth tokens when they expire:
# Refresh FreshBooks tokens
swift run ZohoMigration auth freshbooks
# Refresh Zoho tokens
swift run ZohoMigration auth zoho
# Use custom redirect URI if needed
swift run ZohoMigration auth freshbooks --redirect-uri "https://your-uri.com/callback"The command will guide you through the OAuth flow and automatically update your config.json with the new tokens.
swift run ZohoMigration --help
swift run ZohoMigration migrate --help
swift run ZohoMigration migrate all --helpWhen running migrate all, entities are migrated in this order:
- Categories β Chart of Accounts (needed for expense account mapping)
- Items/Products β Items (skipped by default, use
--include-itemsto enable) - Customers (needed for invoice and payment customer mapping)
- Vendors
- Invoices (uses customer ID mapping; line items created inline)
- Expenses (uses account, vendor, and customer ID mappings)
- Payments (uses customer and invoice ID mappings)
Zoho Books has a rate limit of 100 requests per minute. The tool automatically:
- Tracks request timestamps
- Pauses when approaching the limit
- Displays progress during waits
- Failed entities are logged but don't stop the migration
- Summary shows success/failure counts at the end
- Use
--verbosefor detailed error messages - Archived/deleted entities (visState != 0) are skipped
The tool automatically refreshes OAuth tokens when they expire (401 response).
- The tool maps FreshBooks IDs to Zoho IDs during migration
- Invoice migration requires customer mapping to exist
- Expense migration requires account mapping to exist
- Payment migration requires customer mapping to exist (and optionally invoice mapping)
- Run categories migration before expenses if migrating separately
- Run customers migration before invoices if migrating separately
- Run customers and invoices before payments if migrating separately
- On-the-fly vendor creation: During expense migration, if an expense references a vendor by name that doesn't exist in Zoho, it will be created automatically. Existing vendors are cached at the start to avoid duplicates.
- Invoice terms (payment terms like "Net 30") are mapped from FreshBooks to Zoho