If your organization is still tracking work in Digital.ai Agility, formerly VersionOne, and you have decided to move to Azure DevOps, the decision was the easy part. The migration is where it gets real. You have years of Epics, Stories, Defects, Tasks, and Issues. You have a hierarchy that people actually navigate every day, attachments nobody is willing to lose, and a leadership team that believes "just export it" is a sentence with a meaning.
We finished one of these recently. Then we open sourced the tool that did it.
github.com/accentient/digitalai-agility-to-ado · MIT licensed · PowerShell 7+
It has migrated 53,683 work items in a single run. All types, all scopes, closed items included.
What it moves
The tool reads Agility through its REST API and creates work items in an Azure DevOps project. The type mapping targets the Scrum process:
| Digital.ai Agility | Azure DevOps (Scrum) |
|---|---|
| Epic (Portfolio Item), top level | Epic |
| Epic (Portfolio Item), nested | Feature |
| Story (Backlog Item) | Product Backlog Item |
| Defect | Bug |
| Task | Task |
| Issue | Impediment |
Along with the items themselves it carries across the hierarchy, dependency and Affects links, attachments, area and iteration paths, and a backdated two-point revision history, so created-by and last-changed-by land on their real dates rather than all showing you as the author on migration day. Types, fields, states, and links are all configured in a mappings.json file, so adapting to your instance does not mean editing the script.
Your type names probably lie to you
This one cost us a day, so take it for free. Agility instances can rename asset types in the UI, and the API only ever reports the internal name. No endpoint exposes the alias. The reliable tell is the work item's Number prefix: E- Epic, S- Story, D- Defect, TK- Task, I- Issue, AT- Test, R- Request.
On one instance we migrated, the UI called an Issue a Challenge and called a Defect an Issue. So when the customer said "our Challenges should become Impediments and our Issues should become Bugs," that was already exactly what the table above does. Check the prefixes before you start arguing about mappings.
Four decisions that made it safe
Most of the engineering in a migration tool is not the mapping. It is making the thing safe enough that a nervous customer will actually let you run it against their production instance.
Agility is only ever read from. Every Agility call goes through a single function that hard codes -Method Get. There is no code path in the tool that can modify anything in the source, and the tests assert that in both directions. This is the sentence that ends the risk conversation.
Validate before the first create. Every mapped state and field is checked against Azure DevOps before a single work item is written. A mapping mistake fails on call one, not on item one of fifty thousand.
Dry run everything. -DryRun prints the type, title, parent link, area path, state, priority, and assignee for every item, and asks Azure DevOps to validate each payload with validateOnly=true. Field problems surface as INVALID before anything is written. It is not total coverage, since state transitions, links, and close dates need a real item, but it catches the class of error that would otherwise ruin your weekend.
Deleting is a separate script. The migration only ever creates and updates. Remove-WorkItems.ps1 only ever destroys. The two share no code, no functions, and no names, so no edit to one can change what the other does. Deletion in Azure DevOps is permanent, there is no recycle bin here, and that deserved a wall rather than a flag.
One more property worth calling out: runs are idempotent and resumable. Each item's Agility number is recorded in a custom field, so an interrupted migration just continues where it stopped. On a run of fifty thousand items, that is the difference between a hiccup and a restart.
What it does not do
Honesty is cheaper than a support thread.
It is one way only, Agility to Azure DevOps. There is no sync and no delta mode, so edits made in the source after migration are not brought across. Close dates and the backdated history require a PAT identity with rule-bypass rights. It targets the Scrum process, and the Agile process needs mappings.json changes for User Story and StoryPoints. Epics nested three or more levels deep get flattened onto the top-level Epic, with the real parent preserved as a Related link. Source comments, called Conversations, are not migrated, and history is two-point only, because Agility's full-history endpoint is not available on every hosted instance. Attachments are migrated. Dependency links that form a cycle get rejected by Azure DevOps with TF201035; each one is skipped on its own, so the work items still migrate and only that link is lost.
Versions
Compatibility is governed by the API surface the tool uses, not by the product's branding, which has changed twice. It calls only rest-1.v1/Data and attachment.img with bearer auth, a surface that has been stable across the whole rebranding history. It is verified on Digital.ai Agility 26.1.6.5 SaaS against Azure DevOps Services, and is expected to work on CollabNet VersionOne and legacy VersionOne, though we have not tested those. Azure DevOps Server should work and is likewise untested.
Go take it
Clone it, read docs/migration-reference.md for the full field mapping and the reasoning behind the non-obvious behavior, copy the two sample config files, and run a dry run. The tests are hermetic, so Invoke-Pester -Path tests works without credentials or a live instance. Issues and pull requests are welcome.
And if you are staring down a migration of any real size, know that the script run is rarely the hard part. Scoping, field mapping, process design, and deciding what not to bring across are what actually take the time. If you want help planning or running one, get in touch.
