Claude Code for PowerBuilder (2026)
Why Claude Code for PowerBuilder Modernization
PowerBuilder applications built in the 1990s and 2000s still run critical enterprise systems in insurance, banking, and manufacturing. The proprietary DataWindow technology, PowerScript language, and PBL library format create a migration challenge that no automated tool handles well. Organizations face end-of-support timelines while their PowerBuilder developers retire.
Claude Code understands PowerScript syntax, DataWindow object definitions, and the patterns needed to decompose monolithic PB applications into modern microservices. It can parse DW syntax, map DataWindow column bindings to ORM entities, and translate PowerScript event handlers into equivalent C# or Java code while preserving business logic.
The Workflow
Step 1: Export and Analyze the PBL Libraries
# Export PBL contents using PBExport (Sybase tool) or LibCompare
pbexport /d myapp.pbl /o export_dir/
# Or use the open-source PocketBuilder approach
mkdir -p ~/pb-migration/source ~/pb-migration/output
find /path/to/app -name "*.pbl" -exec cp {} ~/pb-migration/source/ \;
# Create inventory of objects
ls ~/pb-migration/source/export_dir/ | sort > ~/pb-migration/object-inventory.txt
Step 2: Convert DataWindow Definitions to Entity Models
PowerBuilder DataWindows contain SQL, column mappings, and presentation logic in a single object. Claude Code can parse the exported DataWindow syntax and generate equivalent code.
Given a typical DataWindow export like this:
release 12;
datawindow(units=0 timer_interval=0 color=67108864)
table(column=(type=char(50) name=customer_name dbname="customer.cust_name")
column=(type=decimal(2) name=balance dbname="account.balance")
column=(type=datetime name=last_transaction dbname="transaction.txn_date")
retrieve="SELECT customer.cust_name, account.balance, transaction.txn_date
FROM customer
INNER JOIN account ON customer.cust_id = account.cust_id
LEFT JOIN transaction ON account.acct_id = transaction.acct_id
WHERE customer.region = :region
ORDER BY customer.cust_name")
Claude Code generates the C# entity and repository:
// Entity generated from DataWindow: d_customer_balance
public class CustomerBalance
{
[Column("cust_name")]
[StringLength(50)]
public string CustomerName { get; set; }
[Column("balance")]
[Precision(18, 2)]
public decimal Balance { get; set; }
[Column("txn_date")]
public DateTime? LastTransaction { get; set; }
}
// Repository replacing DataWindow retrieve
public class CustomerBalanceRepository : ICustomerBalanceRepository
{
private readonly AppDbContext _context;
public CustomerBalanceRepository(AppDbContext context)
{
_context = context;
}
public async Task<IEnumerable<CustomerBalance>> GetByRegionAsync(string region)
{
return await _context.Customers
.Join(_context.Accounts,
c => c.CustId, a => a.CustId,
(c, a) => new { Customer = c, Account = a })
.GroupJoin(_context.Transactions,
ca => ca.Account.AcctId, t => t.AcctId,
(ca, txns) => new { ca.Customer, ca.Account, Txns = txns })
.SelectMany(x => x.Txns.DefaultIfEmpty(),
(x, t) => new CustomerBalance
{
CustomerName = x.Customer.CustName,
Balance = x.Account.Balance,
LastTransaction = t != null ? t.TxnDate : null
})
.Where(cb => cb.CustomerName != null)
.OrderBy(cb => cb.CustomerName)
.ToListAsync();
}
}
Step 3: Translate PowerScript Event Handlers
// Original PowerScript - w_invoice window
// clicked event on cb_save button
Long ll_invoice_id
Decimal ldec_total
ll_invoice_id = dw_invoice.GetItemNumber(dw_invoice.GetRow(), "invoice_id")
ldec_total = dw_invoice.GetItemDecimal(dw_invoice.GetRow(), "total_amount")
IF ldec_total <= 0 THEN
MessageBox("Validation", "Total must be positive")
RETURN
END IF
IF dw_invoice.Update() = 1 THEN
COMMIT USING SQLCA;
MessageBox("Success", "Invoice " + String(ll_invoice_id) + " saved")
ELSE
ROLLBACK USING SQLCA;
MessageBox("Error", "Save failed: " + SQLCA.SQLErrText)
END IF
Claude Code converts this to a proper service layer:
public class InvoiceService : IInvoiceService
{
private readonly IInvoiceRepository _repository;
private readonly IUnitOfWork _unitOfWork;
public InvoiceService(IInvoiceRepository repository, IUnitOfWork unitOfWork)
{
_repository = repository;
_unitOfWork = unitOfWork;
}
public async Task<Result<Invoice>> SaveInvoiceAsync(long invoiceId, decimal totalAmount)
{
if (totalAmount <= 0)
return Result<Invoice>.Failure("Total must be positive");
try
{
var invoice = await _repository.GetByIdAsync(invoiceId);
invoice.TotalAmount = totalAmount;
await _unitOfWork.CommitAsync();
return Result<Invoice>.Success(invoice);
}
catch (DbUpdateException ex)
{
await _unitOfWork.RollbackAsync();
return Result<Invoice>.Failure($"Save failed: {ex.InnerException?.Message}");
}
}
}
Step 4: Verify Migration Completeness
# Count PowerScript objects vs generated files
echo "PB Objects: $(find ~/pb-migration/source -name '*.sr*' | wc -l)"
echo "Generated C#: $(find ~/pb-migration/output -name '*.cs' | wc -l)"
# Run generated project
cd ~/pb-migration/output
dotnet build --no-restore
dotnet test --verbosity normal
# Verify DataWindow SQL mappings
diff <(grep -r "retrieve=" ~/pb-migration/source/ | sort) \
<(grep -r "// DW:" ~/pb-migration/output/ | sort)
CLAUDE.md for PowerBuilder Migration
# PowerBuilder to C# Migration Standards
## Domain Rules
- Every DataWindow maps to one Entity + one Repository
- PowerScript Transaction objects map to IUnitOfWork pattern
- MessageBox calls convert to Result<T> return types
- Global variables convert to injected services
- PFC (PowerBuilder Foundation Class) patterns map to equivalent .NET patterns
## File Patterns
- Source: *.pbl, *.pbd, *.pbr, *.srw, *.srd, *.srs, *.srf
- Target: *.cs with namespace matching PBL library name
- DataWindows: src/Entities/ and src/Repositories/
- Windows/UserObjects: src/Services/ and src/Controllers/
## Common Commands
- pbexport /d library.pbl /o output/
- dotnet new webapi -n MigratedApp
- dotnet ef migrations add InitialCreate
- dotnet ef database update
- dotnet build --configuration Release
- dotnet test --logger "console;verbosity=detailed"
Common Pitfalls in PowerBuilder Migration
-
DataWindow update properties lost: Claude Code must check the DataWindow’s update characteristics (key columns, updatable columns, where clause type) and replicate them in the EF Core configuration with proper concurrency tokens.
-
Embedded SQL transaction scope: PowerBuilder uses COMMIT/ROLLBACK with named transaction objects (SQLCA). Claude Code wraps these in proper
IUnitOfWorkboundaries instead of letting each repository commit independently. -
Inherited windows and user objects: PowerBuilder’s visual inheritance creates deep chains (w_sheet -> w_master -> w_base). Claude Code flattens these into composition-based C# services rather than replicating fragile inheritance hierarchies.
Related
- Claude Code for COBOL to Java Migration
- Claude Code for VB6 to .NET Migration
- Claude Code for Delphi to C# Conversion
Frequently Asked Questions
Do I need a paid Anthropic plan to use this?
Claude Code works with any Anthropic API plan, including the free tier. However, the free tier has lower rate limits (requests per minute and tokens per minute) that may slow down multi-step workflows. For professional use, the Build or Scale plan provides higher limits and priority access during peak hours.
How does this affect token usage and cost?
The token cost depends on the size of your prompts and Claude’s responses. Typical development tasks consume 10K-50K tokens per interaction. Using a CLAUDE.md file and skills reduces exploration tokens by 50-80%, which directly lowers costs. Monitor your usage at console.anthropic.com/settings/billing.
Can I customize this for my specific project?
Yes. All Claude Code behavior can be customized through CLAUDE.md (project rules), .claude/settings.json (permissions), and .claude/skills/ (domain knowledge). The most impactful customization is adding your project’s specific patterns, conventions, and common commands to CLAUDE.md so Claude Code follows your standards from the start.
What happens when Claude Code makes a mistake?
Claude Code creates files and edits through standard filesystem operations, so all changes are visible in git diff. If a change is wrong, revert it with git checkout -- <file> for a single file or git stash for all changes. Claude Code does not make irreversible changes unless you explicitly allow destructive commands in settings.json.
Practical Details
When working with Claude Code on this topic, keep these implementation details in mind:
Project Configuration. Your CLAUDE.md should include specific references to how your project handles this area. Include file paths, naming conventions, and any project-specific patterns that differ from defaults. Claude Code reads this file at session start and uses it to guide all operations.
Integration with Existing Tools. Claude Code works alongside your existing development tools rather than replacing them. It respects .gitignore for file visibility, uses your project’s installed dependencies, and follows the build/test scripts defined in package.json (or equivalent). Ensure your toolchain is working correctly before involving Claude Code.
Performance Considerations. For large codebases (10,000+ files), Claude Code’s file scanning can be slow if not properly scoped. Use .claudeignore to exclude generated directories (dist, build, .next, coverage) and dependency directories (node_modules, vendor). This typically reduces scan time by 80-90%.
Version Control Integration. All changes Claude Code makes are regular filesystem operations visible to git. Use git diff after each significant change to review what was modified. For experimental changes, create a branch first with git checkout -b experiment/topic so you can easily discard or keep the results.
Build yours → Create a custom CLAUDE.md with our Generator Tool.
Related Guides
Estimate tokens → Calculate your usage with our Token Estimator.
Try it: Paste your error into our Error Diagnostic for an instant fix.