Aha! Develop | Importer extension contributions

Importer contributions give Aha! Develop users a way to bring records one at a time into Aha! Develop from another tool like GitHub, Jira, or Zendesk.

Importers give you visibility into another tool's backlog and create a copy of each record as you pull it into your own Aha! Develop account. From the Zendesk importer, for example, you can import tickets escalated to your development team and track work in Aha! Develop. From Jira, you can select a project and copy in individual stories while you are testing Aha! Develop with your team.

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

Click any of the following links to skip ahead:

Current importers

We have created importers with the following tools. You can always create your own importer extension to a tool not listed here:

Asana importer

Import individual Asana tasks straight to your Aha! Develop backlog.



Azure DevOps importer

Import individual work items from Azure DevOps straight to your Aha! Develop backlog.



GitHub importer

Import individual GitHub issues straight to your Aha! Develop backlog.



GitLab importer

Import individual GitLab issues straight to your Aha! Develop backlog.



Google Sheets importer

Import rows from Google Sheet straight to your Aha! Develop backlog.



Jira importer

Import individual Jira issues straight to your Aha! Develop backlog.



Rally importer

Import individual Rally stories straight to your Aha! Develop backlog.



Salesforce Service Cloud importer

Import Salesforce cases directly to your Aha! Develop backlog.



Sentry importer

Create a feature in Aha! Develop directly from Sentry Issues.



Trello importer

View Trello boards and import individual cards straight to your Aha! Develop backlog.



Zendesk importer

Import individual Zendesk tickets straight to your Aha! Develop backlog.



Administrators in your Aha! Develop account can install these importers by navigating to Plan Sprint planning or Work Board, then clicking Import from the Change view type dropdown in the upper left. Once an administrator has installed an importer extension in your Aha! Develop account, any user in your account can authenticate with the importer, and any user with owner or contributor user permissions can drag work into your Aha! Develop account.



Importer extensions provide a title and an entrypoint. The entrypoint registers callbacks (described in the API section below) to control the behavior of the import and wrap the development tool.

"ahaExtension": {
"contributes": {
"importers": {
"issues": {
"title": "GitHub",
"entryPoint": "src/import.js"



Importer contributions can register callbacks in order to fetch record data to import and customize the import process. All callbacks are optional, other than listCandidates, which must be implemented in order to show importable records.

To register a callback for your importer contribution, first request a reference to it:

const importer = aha.getImporter("aha-develop.github-import.issues");

Once you have the reference, you can register the following callbacks:


The listFilters returns a set of filter definitions that will show up on the import screen to help a user of your extension specify which records to fetch.

importer.on({ action: "listFilters" }, ({}, {identifier, settings}) => {
return {
repo: {
title: "Repository",
required: true,
type: "text",

Filters must have a title and a type. You can also specify whether a filter is required in order to perform a search.



Some filters will require information from the external server. For example, when filtering to an assigned user, you may want to fetch the list of users from the system you are importing from. filterValues returns the list of possible values for a filter field.

importer.on({ action: "filterValues" }, async ({ filterName, filters }, {identifier, settings}) => {
let values = [];
switch (filterName) {
case "repo":
values = await autocompleteRepo(filters.repo);
return values;

filterValues is given two parameters:

  • filterName is the name of the filter whose values are being requested.

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

It should return an array of { text: "...", value: "..." } objects. text will be displayed and value is the value assigned to the filter when it is selected. text is optional — if it is unspecified, value will be displayed instead.



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.



renderRecord can be used to customize how each record is displayed in the import views. If it is unspecified, you will get a simple default card using the name, identifier, and url returned from listCandidates.

// Render a single record.
importer.on({ action: "renderRecord" }, ({ record, onUnmounted }, { identifier, settings }) => {
onUnmounted(() => {
console.log("Un-mounting component for", record.identifier);

return `${record.identifier} ${record.name}`;

renderRecord should return the HTML that will be displayed for the record. The return value can be one of three types:

  • A plain text string will be used as-is. HTML elements in the string will be escaped.

  • A single DOM node or an array of DOM nodes.

  • A React component.

renderRecord takes two parameters:

  • record is the data for the record to be rendered. It is the same record as was returned from listCandidates.

  • onUnmounted takes a function that will be called when the record will no longer appear in the user interface. It can be used to unregister event handlers or perform any other action that is necessary to clean up. For React components, it is not necessary to unmount the component. It will be handled automatically.

See view contributions for more examples of rendering.



The importRecord callback allows you to change a record after it has been imported. It is useful for updating the new record with extra information the extension fetched from the external system:

importer.on({ action: "importRecord" }, async ({ importRecord, ahaRecord }, {identifier, settings}) => {
ahaRecord.name = "[GitHub] " + ahaRecord.name;
await ahaRecord.save();

If you make a change, you are responsible for saving the record.

importRecord takes two parameters:

  • importRecord is the record data from the listCandidates callback.

  • ahaRecord is an ApplicationModel instance corresponding to importRecord.