Aha! Develop | Model API

Models located under aha.models provide a way to work with Aha! Develop records using JavaScript in a natural object-oriented way. Models are an object-oriented wrapper around the Aha! Develop GraphQL API and have properties that reflect the values returned from the API. The set of fields you can request through the Aha! Develop GraphQL API are the same as the set of fields that you can select on the models. The model API provides an interface that allows you to communicate with the Aha! Develop GraphQL API in a way that should feel familiar if you've used an ActiveRecord-style ORM.

Click any of the following links to skip ahead:


For example, you can find a feature from the API returning a feature instance with id, name, and referenceNum properties:

const feature = await aha.models.Feature.select('id', 'name', 'referenceNum').find(featureId)

You can build queries incrementally and conditionally:

let featureScope = aha.models.Feature.select('id', 'name', 'referenceNum').where(releaseId: release.id);

if (sorted) {
featureScope = featureScope.order({workflowBoardPosition: 'ASC'});

const featurePage = await featureScope.all();
const features = featurePage.models;

The returned objects can provide extra functionality through their instance methods and properties:

return <p>{feature.titleWithReferenceNum}</p>;

You can modify the objects and save changes back to Aha! Develop:

feature.name = "A new name";
const success = await feature.save();


Models in detail

A model is an object that inherits from ApplicationModel:

import { model } from '../modelBuilder';
import ApplicationModel from '../ApplicationModel';

class Feature extends ApplicationModel {
static typename = 'Feature';

export default model(Feature);

It maps to a server-side GraphQL type using typename.

GraphQL responses made through the model API will automatically return models of the correct type:

import { model } from '../modelBuilder';
import ApplicationModel from '../ApplicationModel';

class Iteration extends ApplicationModel {
static typename = 'Iteration';

static STATUS_PLANNING = 10;
static STATUS_STARTED = 20;

get planning() {
return this.status === Iteration.STATUS_PLANNING;

export default model(Iteration);

// later...

const iteration = await Iteration.select('id', 'name', 'status').find(iterationId);

This is made possible through the __typename fields that GraphQL returns.

Model queries

From a model class, you can build scopes that you can use to make queries. GraphQL requires you to be explicit about which fields you want to be returned, so you use select to start building a scope:

const scope = Feature.select('id', 'name').where({ projectId });

// Executes the query, returning multiple records (or a page of records):
await scope.all()

// Executes the query, finding a single record:
await scope.find(featureId)

At a later point in time, you can load additional fields into the model. This is especially useful if you have received a model and need to access fields that were not included:

const feature = Feature.select('id').find(featureId);

// feature does not have the name field loaded
feature.id // undefined

// Load the name and referenceNum fields
await feature.loadAttributes('name', 'referenceNum');
feature.name // 'Feature 1'

Scopes provide basic ActiveRecord-style methods select, where, order:

.select('id', 'name')
.where({ projectId })
.where({ active: true })
.order({ position: "ASC" });

Each of these methods returns a new scope instance so you can save partial scopes and build on them later.

You can select trees of models using merge with either a list of fields or a query:

const scope = Feature.select('id', 'name');

// By a list of fields
scope.merge({ requirements: ['id', 'name']});

// By a subquery
scope.merge({ requirements: Requirement.select('id', 'name').where({ active: true })});

GraphQL also has the ability to return a list with elements of multiple types. One example is “records,” which is a list of features, epics, and requirements. You can build these queries with union:

Iteration.select('id', 'name')
records: Feature.select('id', 'name').union(Epic.select('id', 'name'))

These unions are typed so records will come back as the correct model class.


Most requests that return lists are paginated and all() will return a page of records instead of a list. If you have a page of records, you can access the underlying records using models:

const features = await Feature.select('id', 'name').all()

You can fetch stats about the page (isLastPage, totalCount, totalPages, etc.), which are returned on the page record:

await Feature.select('id', 'name').stats(['isLastPage', 'totalCount']).all()

And change the page parameters using page and per:

await Feature.select('id', 'name').page(1).per(20).all()


Once you have a record and you’ve modified it, you can persist your changes using save:

feature.name = "A new name";
const success = await feature.save();

save will send all changed fields back to the server and update the object with the current state from the server. Currently changed means object identity but this will be improved later on.

save returns true on success and false on failure. If the save fails, error messages can be read through the .errors property on the model.