Aha! Develop | Build your first importer extension

Importer extensions connect your Aha! Develop account with another tool so you can pull individual records into your account. Importers could help you create user stories from customer support tickets that have been escalated to your team or trial Aha! Develop before migrating your backlog in full.

We have created importer extensions with some commonly used tools. If you do not see your tool of choice on that list, you do not need to wait. Follow these steps to create your own importer extension. It is as simple as filling in the aha-cli example file.

The intent of importer contributions is not to create an ongoing integration or to import an entire backlog at once. For that, use the CSV import.

Click any of the following links to skip ahead:

Getting started

Importer extensions can be implemented in stages. In this article, we will discuss a very quick basic implementation, then move on to advanced functionality that you can implement.

Please see Aha! web components for common UI elements that you can use inside your extensions.

Before you proceed, make sure:

You can also investigate the source code for importer extensions we authored ourselves:

  • GitHub importer — Good balance between simple and interesting functionality.

  • Trello importer — Good example of a very detailed importer.

  • Asana importer — Robust and easy to extend.

In this article, we will use our Asana importer for example field values. Follow these steps to communicate with your tool of choice.


Generate an example file with aha-cli

The aha-cli can generate an example file for you. Once you fill it in, your importer extension will be ready to go.

To generate an example file, enter aha extension:create.

You will see the following example file:

~/Source jweiss$ aha extension:create
? Enter a human readable name for your extension: Asana importer
? Who are you? Your personal or organization GitHub handle is a good identifier: justinweiss
? Each extension must have a universally unique identifer that is also a valid NPM package name.
Generally a good identifier is <organization-name>.<extension-name>.
Enter an identifier: aha.asana-importer
? Are you ready to add contributions? yes

? Enter a human readable title for your contribution: Asana importer
? Enter a name for your contribution: asanaImporter
? Select a type for your contribution: importer
? Enter an entry point for your contribution: src/importers/asanaImporter.js
? Add another contribution? no
Creating extension... Extension created in directory 'asana-importer'

Replace our example text with your own.


Basic implementation

To get an importer extension up and running quickly, all you need to do is implement listCandidates. The listCandidates generated by the example file returns an empty list. Your job is to fill that list by communicating with a service to retrieve records to import.

Records in this sense are JavaScript objects with any properties you want. These records will be passed to other functions in this importer file, so store any data you might need later. Each record must have at least a uniqueId field and a name field.

It is best to limit the number of records you return, though Aha! Develop will handle showing pages of records when necessary. nextPage is any data you need to fetch the next page of records. The nextPage you return will be passed back into listCandidates when requesting the next page.


listCandidates returns a page of record data that can be imported into Aha! Develop:

importer.on({ action: "listCandidates" }, async ({ filters, nextPage }, {identifier, settings}) => {
return findIssues(filters.repo, nextPage);

It must return an object:

{ records: [...], nextPage: ... }

records is a list of objects. Each record must have a uniqueId field and a name field but the rest of the information is up to you. Your record will be passed into other callbacks so it can store data you need for those.

There are special fields identifier and url that are used for display if the renderRecord callback is not registered.

nextPage is an arbitrary identifier that you will use to fetch the next page. For some systems, it could be a page number. For other systems, it could be a cursor. Whatever you return in the nextPage field will be passed back into this function when the next page is fetched. Set nextPage to null when there is no more data to return.

listCandidates is given two parameters:

  • filters is the current values set to each of your extension's filters.

  • nextPage is the value your extension returned as nextPage from the last time this function was called. It will be null whenever a completely new set of records are fetched.

Test your importer

You will need to be an administrator with customization privileges to install your importer. To test it, consider using aha extension:watch. watch will install your importer in Aha! Develop but only enable it for your user (and not prompt the other users in your account). Then it will update the code in Aha! Develop in real-time as you make changes on your dev machine. It will watch for changes and re-upload to Aha! Develop whenever the code changes.

Once you have installed your importer, try it out! Navigate to Plan Sprint planning or Work Board, then click Import from the Change view type dropdown in the upper left.

When you are ready for other users in your account to access the importer, navigate to Settings ⚙️ Account Extensions and click on your importer extension. Click the Permission prompt toggle to notify all users in your account about your new importer.

All users in your account can enable and use extensions that are already installed, including importers.


Advanced implementation

A basic importer extension will authenticate with your tool of choice and allow you to find and copy records into your Aha! Develop account. If you are going to use your importer heavily, however, you may want to consider the following advanced functionality:

Filter values

Use filters to narrow your list of issues — by a specific project, tags, status (e.g. work that is still in progress), assignee, or anything else that comes to mind.

To do this, first implement the listFilters and filterValues importer callbacks.

listFilters is called by Aha! to get a list of possible filters that can be shown in the UI. Each filter is a plain JavaScript object with a title, a type, and whether the filter is required.

type can be:

  • select (a normal select-style dropdown list)

  • autocomplete (a field that dynamically shows options based on what's typed into the field)

  • text (a plain text field)

For select and autocomplete, the list of options is retrieved by calling into the filterValues importer callback.

filterValues takes a filterName (the same as the key of one of the filters in listFilters ) and the set of all current filter values entered by the user. This is so that lists of filters can depend on values already used. For example, if you are listing a set of tags, you may need to know which project the user has selected in order to show the right tag set.

filterValues returns a list of objects with text (text displayed to the user) and value (value assigned to the filter) keys.

Finally, these filters are passed into listCandidates so you can use the filters to decide which records to fetch and return to Aha! Develop.


Import extra detail

When you import a record, a new record is created in Aha! Develop with the name you gave in listCandidates. If you want to add more detail — add a description, import comments, import requirements — you can do whatever after-import tasks you want in importRecord.

importRecord is given the same record data returned from listCandidates along with the newly imported record in Aha! Develop. This Aha! Develop record is an aha.models.Feature from the Aha! Model API and can be used in the same way. Optionally, you can use the Aha! GraphQL API or Aha! REST API directly.

The GitHub import extension is a simple example of copying the description into Aha! Develop, along with a link to the original record in GitHub.

The Trello import extension is an example of a much more detailed importRecord, which converts descriptions to HTML from Markdown, imports tags, checklists, and more.


Change card display

Finally, you can adjust the look and feel of the records you import with renderRecord.

For example, the Trello importer renders cards with the name of the Trello list each record belongs to:

{ action: "renderRecord" },
({ record, onUnmounted }, { identifier, settings }) => {
return (
<span className="text-muted">{record.list}</span>
<br />
<a href={aha.sanitizeUrl(record.url)} target="_blank" rel="noopener">

renderRecord can return a plain text string, DOM nodes, or a React component.