Aha! Develop trials are currently invitation-only. If your team is interested, apply for early access now.

Aha! Develop | Importer extension contributions

Importer contributions give Aha! Develop users a way to bring records into Aha! Develop from another development tool or service like GitHub or Jira.

Click any of the following links to skip ahead:


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 issues",
"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.


Related articles

Aha! Roadmaps
Aha! Ideas
© 2021 Aha! Labs Inc.All rights reserved