Developer
News and Updates
Get Support
Sign in
Get Support
Sign in
DOCUMENTATION
Cloud
Data Center
Resources
Sign in
Sign in
DOCUMENTATION
Cloud
Data Center
Resources
Sign in
Last updated Aug 21, 2026

Querying the Custom Entity Store via the legacy package (unsupported)

This page explains how Custom Entity queries were built using the storage module from the @forge/api package.

This module is no longer supported or available as of June 22, 2026. The information on this page is only being provided as a reference for developers migrating to the @forge/kvs package.

If your app already uses the supported @forge/kvs package, see Querying the Custom Entity Store instead.

If you are still using the legacy storage module and need to migrate, see Migrating to @forge/kvs from unsupported storage module.

The legacy storage module supported building complex queries against data stored in the Custom Entity Store using a wide variety of filters and conditions.

The Forge API package was imported as follows:

1
2
import { storage } from '@forge/api';

Each installation of an app is subject to the API's quotas and limits. See Storage quotas and Storage limits for more details.

Scope requirement

Using Forge’s persistent hosted storage through the @forge/api package required the storage:app scope in the manifest file:

1
2
3
4
permissions:
  scopes:
    - storage:app

For more information about scopes, see Permissions.

Before storing data in the Custom Entity Store, apps declare custom entities and indexes in the manifest file. Custom entities are user-defined data structures for storing app data. Forge's storage API lets you query data stored in these structures using a wide array of query conditions. These query conditions make it possible to build advanced, complex queries to suit your app's operations.

For information about storing data to the Custom Entity Store, see Storing data in custom entities .

For a detailed tutorial on storing and querying structured data through custom entities, see Use custom entities to store structured data.

Basic methods

All complex queries operate on a custom entity's index. Complex queries followed the same basic signature:

1
2
3
4
5
await storage
  .entity("<custom-entity>")
  .query()
  .index()

This structure contained all the required methods for a complex query. The entity method set which custom entity to query, and index set which of that entity's indexes to query. Each query could only target one index from one custom entity.

When using indexes that featured a partition, a value had to be specified to match the parameter's attribute:

1
2
3
4
5
6
7
await storage
  .entity("<custom-entity>")
  .query()
  .index("<index-name>", {
    partition: ["<value>"]
  })

If an index's partition had multiple attributes, a value had to be set for each attribute, in the order they were declared in the index. For example, consider the following index:

1
2
3
4
5
6
7
8
indexes:
  - name: by-gender-and-age
    range: 
      - employmentyear
    partition: 
      - gender
      - age

An appropriate query for this index would have been:

1
2
3
4
5
6
7
8
9
await storage
  .entity("employee")
  .query()
  .index("by-gender-and-age", {
    partition: ["male", 20]
  })
  .where(WhereConditions.isGreaterThan(2003))
  .getMany()

This query fetched employees who were:

  • male
  • 20 years old
  • employed after 2003 (that is, employmentyear is higher than 2003).

Every complex query returned up to 10 values by default. This could be increased to a maximum of 100 using query.limit.

where

While index filtered matches to an index's partition, where filtered against an index's range. The where filter was imported as follows:

1
2
import { WhereConditions } from '@forge/api';

Method signature

1
2
.where(WhereConditions.<condition>("<value>"))

Conditions

The where filtering method supported the following conditions:

  • beginsWith
  • between
  • equalsTo
  • isGreaterThan, isLessThan
  • isGreaterThanOrEqualTo, isLessThanOrEqualTo

andFilter / orFilter

The index and where methods could only be used once per query. The andFilter and orFilter methods allowed additional conditions to be added to a query.

Method signature

Either filtering method was imported as follows:

1
2
import { FilterConditions } from '@forge/api';

Each filtering method used the following signatures:

  • andFilter: all conditions had to be matched.
    1
    2
    .andFilter("<attribute>", FilterConditions.<condition>("<value>"))
    
  • orFilter: only one condition had to be matched.
    1
    2
    .orFilter("<attribute>", FilterConditions.<condition>("<value>"))
    

Within the same query, multiple andFilter or orFilter methods could be used. However, both methods could not be used within the same query.

In addition, the andFilter and orFilter methods were in-memory filters. Using them could sometimes produce pages with no results, with the cursor pointing to the next page where actual results existed.

Conditions

Both filtering methods supported the following conditions:

  • beginsWith
  • between
  • equalsTo
  • isGreaterThan, isLessThan
  • isGreaterThanOrEqualTo, isLessThanOrEqualTo
  • exists, doesNotExist
  • contains, doesNotContain
  • notEqualsTo

sort

The sort method displayed results in either ascending (ASC) or descending (DESC) order:

1
2
.sort(SortOrder.<"ASC|DESC">)

By default, results were displayed in ascending order.

query.limit

Returns a new Query with a limit on how many matching values get returned. The query API returns up to 10 values by default, this can be increased to a maximum of 100.

Method signature

1
2
query.limit(limit: number): Query

query.cursor

Returns a new Query that will start after the provided cursor. Cursors enable your app to fetch subsequent pages of results after completing an initial query.

Cursors are returned from the getMany query API.

Method signature

1
2
query.cursor(after: string): Query;

query.getMany

Execute the query and return a list of results up to the provided limit in length. This method returns both the array of results and a cursor that's used to fetch subsequent pages of results.

Method signature

1
2
3
4
5
6
7
8
9
10
11
12
query.getMany(): Promise<ListResult>;

interface ListResult {
  results: Result[];
  nextCursor?: string;
}

export interface Result {
  key: string;
  value: object;
}

query.getOne

Execute the query and get the first matching result, if any matches exist. If there is no match, the result resolves to undefined.

Method signature

1
2
3
4
5
6
7
query.getOne(): Promise<Result | undefined>;

export interface Result {
  key: string;
  value: object;
}

Example entity

The following manifest.yml excerpt shows a custom entity named employee with several attributes and indexes:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
app:
  id: "ari:cloud:ecosystem::app/406d303d-0393-4ec4-ad7c-1435be94583a"

  storage:
    entities:
      - name: employee
        attributes:
          surname: 
            type: string
          age: 
            type: integer
          employmentyear: 
            type: integer
          gender: 
            type: string
          nationality: 
            type: string
        indexes:
          - surname
          - employmentyear
          - name: by-age
            range: 
              - age
          - name: by-age-per-gender
            partition: 
              - gender
            range: 
              - age  

This entity also creates four indexes based on the following employee attributes:

  • surname
  • employmentyear
  • age (further optimized for filtering according to different age ranges)
  • age per gender (further optimized for filtering according to age ranges for each gender)

Example queries

Using the previous section's example entity and its indexes, the following queries demonstrated the use of each method:

Simple index

Targeted the surname index of the employee entity.

1
2
3
4
5
6
await storage
  .entity("employee")
  .query()
  .index("surname")
  .getMany()

Simple index with where condition

Targeted the by-age index, which used age as its range. The where method limited matches to employees above the age of 30. Results were displayed in descending order.

1
2
3
4
5
6
7
await storage
  .entity("employee")
  .query().index("by-age")
  .where(WhereConditions.isGreaterThan(30))
  .sort(SortOrder.DESC)
  .getMany()

Named index with partition

Targeted the by-age-per-gender index, and limited matches to female employees.

1
2
3
4
5
6
7
8
await storage
  .entity("employee")
  .query()
  .index("by-age-per-gender", {
    partition: ["female"]
  })
  .getMany()

Named index with partition and filters

Using the by-age-per-gender index, this query limited matches to female Australian employees above the age of 30 who were hired after 2020.

1
2
3
4
5
6
7
8
9
10
11
await storage
  .entity("employee")
  .query()
  .index("by-age-per-gender", {
    partition: ["female"]
  })
  .where(WhereConditions.isGreaterThan(30))
  .andFilter("employmentyear", FilterConditions.isGreaterThan(2020))
  .andFilter("nationality", FilterConditions.equalsTo("Australian"))
  .getMany()

Rate this page: