User-defined custom entity APIs - use cases, sample requests

User-defined custom entity APIs - use cases, sample requests

For an overview of custom entities:

Use cases for custom entities:

  1. https://folio-org.atlassian.net/wiki/spaces/FOLIOtips/pages/1558413333/User-defined+custom+entity+APIs+-+use+cases+sample+requests#Use-case-1%3A-create-a-custom-entity-for-Instances-with-holdings

  2. https://folio-org.atlassian.net/wiki/spaces/FOLIOtips/pages/1558413333/User-defined+custom+entity+APIs+-+use+cases+sample+requests#Use-case-2%3A-update-custom-entity-for-Instances-with-holdings

  3. https://folio-org.atlassian.net/wiki/spaces/FOLIOtips/pages/1558413333/User-defined+custom+entity+APIs+-+use+cases+sample+requests#Use-case-3%3A-view-custom-ET-definition

  4. https://folio-org.atlassian.net/wiki/spaces/FOLIOtips/pages/1558413333/User-defined+custom+entity+APIs+-+use+cases+sample+requests#Use-case-4%3A-delete-a-custom-ET

Postman collection with sample requests

The linked Postman collections contains example requests to the available joins endpoint for the Eureka snapshot environment. These can be used to test this endpoint on Eureka snapshot.

Screenshot 2026-01-15 122804.png
Postman collection with Custom ET CRUD endpoints, including sample requests

High level overview of endpoints

Custom ET (Create Read Update Delete) CRUD endpoints

These endpoints are to create, view, edit and delete custom ETs in FQM: an entity defines the data is available in a given view, also referred to as an entity type (or abbreviated to ET). Entities display as the “Record type” in the Lists app, and are queryable through FQM APIs. The Create custom ET API allows authorized users to combine existing data/record types into custom entities.

  • Create a custom entity: POST /entity-types/custom

  • View a custom entity: GET /entity-types/custom/{entityTypeId}

  • Update a custom entity: PUT /entity-types/custom/{entityTypeId}

  • Delete a custom entity: DEL /entity-types/custom/{entityTypeId}

Available joins API:


Use case 1: create a custom entity for Instances with holdings

I want to build a list of instances that returns holdings data

Steps and example request to create a Custom ET:

  • POST to /entity-types/custom

    • the request body:

      • the id can be left blank in the initial POST; it is provided in the response

      • provide a name for the ET; when testing in snapshot, please include TEST or something to the name to indicate it’s experimental

      • optionally provide a description

      • shared: true/false, determines if other users can access this ET

      • private: true/false, determines if it appears in the Lists app

      • sources:

        • you can use the source definitions from the getAvailableJoins endpoint to help populate request the body

        • at least one source needs to have useIdColumns : true

    • Example body of a request to create a custom ET:

    • { "id": "", //the id can be left blank in the initial POST; it is provided in the response "name": "Test Instance with holdings", //Provide name here "description": "Returns instances with holdings data, if it's available", //Provide a description here "labelAlias": null, "shared": false, //determines if other users can access this ET "private": false, //false means it appears in the Lists app "sources": [ { "type": "entity-type", "alias": "simple_instance", //used to identify fields from this source "name": "Instance", //user facing name for this source; fields from this ET display with "name" as prefix "targetId": "8fc4a9d2-7ccf-4233-afb8-796911839862", //UUID for the base source, in this case simple instance "essentialOnly": true, "useIdColumns": true }, { "type": "entity-type", "alias": "simple_holdings", //used to identify fields from this source "name": "Holdings", //user facing name for this source; fields from this ET display with "name" as prefix "targetId": "30a5cfad-1868-4f46-86b9-a6ef67e2d9bf", //UUID for the source, in this case simple holdings "sourceField": "simple_instance.id", //getAvailableJoins endpoint helps identify the correct sourceField name "targetField": "instance_id", //getAvailableJoins endpoint helps identify the correct targetField name "essentialOnly": true, "useIdColumns": true } ] }
    • To define sources, use the available joins API

  • Response: a valid request returns a 201 Created. the body of the response includes the newly created custom ET, including the id and additional properties added by default

    • take note of the id as that how you’ll be able to view or modify your newly created entity

    • example response:

    • { "id": "60f48cd8-9e9e-492b-aa6d-9fd3b87ac604", "name": "Test Instance with holdings", "description": "Custom ET that returns Instances with holdings data, when available", "labelAlias": null, "customFieldEntityTypeId": null, "columns": [], "crossTenantQueriesEnabled": false, "defaultSort": null, "fromClause": null, "idView": null, "groupByFields": null, "sourceView": null, "sourceViewExtractor": null, "sources": [ { "type": "entity-type", "alias": "simple_instance", "name": "Instance", "joinedViaEntityType": null, "order": 100, "sourceField": null, "targetId": "8fc4a9d2-7ccf-4233-afb8-796911839862", "targetField": null, "overrideJoinDirection": null, "useIdColumns": true, "essentialOnly": true, "inheritCustomFields": true }, { "type": "entity-type", "alias": "simple_holdings", "name": "Holdings", "joinedViaEntityType": null, "order": 200, "sourceField": "simple_instance.id", "targetId": "30a5cfad-1868-4f46-86b9-a6ef67e2d9bf", "targetField": "instance_id", "overrideJoinDirection": null, "useIdColumns": true, "essentialOnly": true, "inheritCustomFields": true } ], "requiredPermissions": [], "filterConditions": [], "additionalEcsConditions": [], "deleted": false, "usedBy": [], "owner": "76edcb72-77b3-4922-952a-6a9d99c69e8c", "shared": false, "createdAt": "2025-12-31T17:05:52.676+00:00", "updatedAt": "2025-12-31T17:05:52.676+00:00", "isCustom": true, "private": false }
  • if the ET is set to private: false you'll be able to access and test it in the Lists app at this point!

Steps to view the custom ET in the lists app

  • Preconditions:

  • Open the Lists app

    • Click “New” - provide a list name

    • In the “Record type” dropdown, an option should be available that matches the name in the request to create a custom ET - select that name

    • Click “Build query” > the Query Builder opens

    • When you click on the “Select field” dropdown, you should see a list of available fields that can be used to query the new ET

      • the name for each source should prepend available field names (e.g., “Instance – Administrative notes” or “Holdings – Created date”)

      • build a query with your desired parameters

    • Click “Test query”

    • See short recording to see how things look in the Lists app at this point

      • note the second query returned no results - indicating there are no instances without holdings, or, that there’s something potentially wrong with this custom ET.

      • in this case the custom ET is only returning Instances that have associated holdings, which isn’t the desired behavior.

    • Custom instances with holdings 1.mp4
      Viewing a custom ET in the Lists app - returns instances that have holdings

Use case 2: update custom entity for Instances with holdings

I want to build a list of instances that returns holdings data when it’s available. I want to be able to query instance records that don’t have associated holdings records.

Steps to update the custom ET definition

  • PUT to /entity-types/custom/{entityTypeId}

    • {entityTypeId} should be replaced with the id from the response when you created the custom ET; in this example it would be 60f48cd8-9e9e-492b-aa6d-9fd3b87ac604

    • the request body:

      • includes the ET definition

      • we want to update this custom ET so that it will return all instances, and will include holdings data if it’s available

        • this can be done by updating the overrideJoinDirection for the holdings source to left (row 39 below)

      • example request body:

      • { "id": "60f48cd8-9e9e-492b-aa6d-9fd3b87ac604", "name": "Test Instance with holdings", "description": "Custom ET that returns Instances with holdings data, when available", "labelAlias": null, "customFieldEntityTypeId": null, "columns": [], "crossTenantQueriesEnabled": false, "defaultSort": null, "fromClause": null, "idView": null, "groupByFields": null, "sourceView": null, "sourceViewExtractor": null, "sources": [ { "type": "entity-type", "alias": "simple_instance", "name": "Instance", "joinedViaEntityType": null, "order": 100, "sourceField": null, "targetId": "8fc4a9d2-7ccf-4233-afb8-796911839862", "targetField": null, "overrideJoinDirection": null, "useIdColumns": true, "essentialOnly": true, "inheritCustomFields": true }, { "type": "entity-type", "alias": "simple_holdings", "name": "Holdings", "joinedViaEntityType": null, "order": 200, "sourceField": "simple_instance.id", "targetId": "30a5cfad-1868-4f46-86b9-a6ef67e2d9bf", "targetField": "instance_id", "overrideJoinDirection": "left", "useIdColumns": true, "essentialOnly": true, "inheritCustomFields": true } ], "requiredPermissions": [], "filterConditions": [], "additionalEcsConditions": [], "deleted": false, "usedBy": [], "owner": "76edcb72-77b3-4922-952a-6a9d99c69e8c", "shared": false, "createdAt": "2025-12-31T17:05:52.676+00:00", "updatedAt": "2025-12-31T17:05:52.676+00:00", "isCustom": true, "private": false }
  • Response:

    • 200 OK and the custom ET is updated

View the updated custom ET in the lists app

  • As you did in a previous step, create a list with the custom entity type

  • Build a query

    • Instance – Source = MARC

    • AND Holdings – UUID is null/empty true

  • Test the query

  • Expected results:

    • Instances with source MARC but without associated holdings appear in the results

    • Custom instances with holdings 2.mp4
      Viewing a custom ET in the Lists app - returns Instances with holdings data, when available

Use case 3: view custom ET definition

I want to see what sources/settings are used for a specific custom ET. I can then use the response to update the custom ET if needed.

Steps to view the custom ET definition

  • GET to /entity-types/custom/{entityTypeId}

    • {entityTypeId} should be replaced with the id from the response when you created the custom ET; in this example it would be 60f48cd8-9e9e-492b-aa6d-9fd3b87ac604

    • the request body is not needed for this request

  • Response:

    • { "id": "60f48cd8-9e9e-492b-aa6d-9fd3b87ac604", "name": "Test Instance with holdings", "description": "A custom ET that will return Instances with Holdings data, if it's available", "labelAlias": null, "customFieldEntityTypeId": null, "columns": [], "crossTenantQueriesEnabled": false, "defaultSort": null, "fromClause": null, "idView": null, "groupByFields": null, "sourceView": null, "sourceViewExtractor": null, "sources": [ { "type": "entity-type", "alias": "simple_instance", "name": "Instance", "joinedViaEntityType": null, "order": 100, "sourceField": null, "targetId": "8fc4a9d2-7ccf-4233-afb8-796911839862", "targetField": null, "overrideJoinDirection": null, "useIdColumns": true, "essentialOnly": true, "inheritCustomFields": true }, { "type": "entity-type", "alias": "simple_holdings", "name": "Holdings", "joinedViaEntityType": null, "order": 200, "sourceField": "simple_instance.id", "targetId": "30a5cfad-1868-4f46-86b9-a6ef67e2d9bf", "targetField": "instance_id", "overrideJoinDirection": null, "useIdColumns": true, "essentialOnly": true, "inheritCustomFields": true } ], "requiredPermissions": [], "filterConditions": [], "additionalEcsConditions": [], "deleted": false, "usedBy": [], "owner": "25f4732d-9999-4f20-9ebe-509c69c1437a", "shared": false, "createdAt": "2026-01-15T19:43:24.592+00:00", "updatedAt": "2026-01-15T19:52:23.666+00:00", "isCustom": true, "private": false }

Use case 4: delete a custom ET

There’s a custom ET available to my institution that doesn’t work, or that needs to be deleted for another reason.

Steps to delete a custom ET definition

note: this is a soft delete

  • DELETE to /entity-types/custom/{entityTypeId}

    • {entityTypeId} should be replaced with the id from the response when you created the custom ET; in this example it would be 60f48cd8-9e9e-492b-aa6d-9fd3b87ac604

    • the request body is not needed for this request

  • Response:

    • a successful delete returns a 204 No Content

    • if the ET is being used by a List or another ET, it will not allow you to delete the ET

      • it will return a 409 conflict

  • to restore a deleted custom ET:

    • use the updateCustomEntityType endpoint

      • as part of the body of the request

      • "deleted": false


Additional details / example of defining sources

  • Use case 1: POST to /entity-types/custom/available-joins with an empty body {}

    • in the results, note the value for “Instances” in the availableTargetIds

  • Use case 2: POST to /entity-types/custom/available-joins with a source in the body
    (the example in the available joins use case 2 is already populated for instances)

    • request body:

      { "sources": [ { "type": "entity-type", "alias": "simple_instance", "name": "Instance", "targetId": "8fc4a9d2-7ccf-4233-afb8-796911839862", //simple instances "essentialOnly": true } ] }
    • in the results, note the value for “Holdings” in the availableTargetIds

  • Use case 3: POST to /entity-types/custom/available-joins with the source from the previous step, adding a targetId for Holdings

    • request body:

      { "sources": [ { "type": "entity-type", "alias": "simple_instance", "name": "Instance", "targetId": "8fc4a9d2-7ccf-4233-afb8-796911839862", //simple instances "essentialOnly": true } ], "targetId": "30a5cfad-1868-4f46-86b9-a6ef67e2d9bf" //simple holdings }
    • response body:

      { "availableTargetIds": null, "availableJoinConditions": [ { "sourceField": { "label": "Instance — Instance UUID", "value": "simple_instances.id" }, "targetField": { "label": "Instance UUID", "value": "instance_id" } } ] }
  • Use the details returned in use case #3 to populate a second complete source (holdings)

    • example sources:

      { "sources": [ { "type": "entity-type", "alias": "simple_instance", //used to identify fields from this source "name": "Instance", //user facing name for this source; fields from this ET display with "name" as prefix "targetId": "8fc4a9d2-7ccf-4233-afb8-796911839862", //UUID for the base source, in this case simple instance "essentialOnly": true, "useIdColumns": true }, { "type": "entity-type", "alias": "simple_holdings", //used to identify fields from this source "name": "Holdings", //user facing name for this source; fields from this ET display with "name" as prefix "targetId": "30a5cfad-1868-4f46-86b9-a6ef67e2d9bf", //UUID for the source, in this case simple holdings "sourceField": "simple_instance.id", //getAvailableJoins endpoint helps identify the correct sourceField name "targetField": "instance_id", //getAvailableJoins endpoint helps identify the correct targetField name "essentialOnly": true, "useIdColumns": true } ] }