Custom User Ingestion with Identity Gateway

Configure Identity Gateway to ingest users from any system that exposes an HTTP API.
View as Markdown

Overview

Identity Gateway lets you ingest user identity data from any system that exposes an HTTP API into Moveworks. Use it when your identity source (for example, a ServiceNow HR profile, Workday, or a custom HRIS) is not natively supported as a built-in identity ingestion source.

Setting it up involves three pieces:

  1. An HTTP Action that calls your API.
  2. A Crawl Config that handles pagination and extracts records from the response.
  3. An Identity Ingestion Source that maps the extracted data into the Moveworks User Profile.

Architecture Overview

Data flows through these components in order:

ComponentPurpose
HTTP ActionDefines the API call (endpoint, method, query parameters)
Crawl ConfigOrchestrates pagination, extracts records from the API response, and wraps them for the identity pipeline
Identity Ingestion SourceMaps the extracted records to Moveworks User Profile fields

Prerequisites

  • A connector configured via HTTP Connectors for the identity platform you’re ingesting.
  • An HTTP Action configured in Agent Studio that leverages your connector and fetches users from your identity provider’s API.

Step 1: Create the HTTP Action

Create an HTTP Action that calls your identity source API. The action should:

  • Use the Integration linked to your connector.
  • Accept pagination parameters as Input Args (for example, sysparm_offset, sysparm_limit, or page and page_size).
  • Pass those Input Args as query parameters in the request.
  • If you are sending parameters in the body, you must use Data Mapper — raw JSON bodies are not currently supported for this use case.

Example: For a ServiceNow HR Profile API, create an action named get_hr_profile with two Input Args: sysparm_offset and sysparm_limit. These get passed as query parameters so the API returns paginated results.

Example API Response

Your HTTP Action should return a JSON response like this:

1{
2 "result": [
3 {
4 "user.sys_id": {
5 "display_value": "67ab2e2493d88310df50fc8bdd03d6fb",
6 "value": "67ab2e2493d88310df50fc8bdd03d6fb"
7 },
8 "user.email": {
9 "display_value": "jdoe@company.com",
10 "value": "jdoe@company.com"
11 },
12 "employment_start_date": {
13 "display_value": "2022-06-06",
14 "value": "2022-06-06"
15 },
16 "employee_number": {
17 "display_value": "EMP001",
18 "value": "EMP001"
19 },
20 "user.user_name": {
21 "display_value": "john.doe",
22 "value": "john.doe"
23 }
24 }
25 ]
26}

Note: The exact response structure depends on your system. The key thing is to know (1) the path to the array of user records and (2) the path to each field within a record. You will use this in the next steps.

Step 2: Create the Crawl Config

Create a new Identity Gateway ingestion at User Identity > Identity Gateway > Create New Configuration. This ties together the HTTP Action, pagination logic, and data extraction. It has three sections.

2a. Integration ID

Set this to the same Integration used by your HTTP Action.

2b. Start Request (Initial API Call)

This defines the first API call to kick off ingestion.

FieldDescription
Action IdSelect your HTTP Action (for example, get_hr_profile)
Input ArgsA data mapping expression that provides initial values for the action’s Input Args

Input Args Example — sets the first page to offset 0 with 100 records per page:

1{
2 "sysparm_offset": "'0'",
3 "sysparm_limit": "'100'"
4}

Values must be wrapped in single quotes inside the expression (for example, "'0'", not "0"). The single quotes denote a string literal in the Moveworks data mapping language. Also confirm on the HTTP Action that your input args are not hardcoded and are dynamic — your query parameters should look like {{page}} in the HTTP Action.

2c. Response Handler

The Response Handler has two parts: the Output Bender (data extraction) and the Next Request Details (pagination).

Output Bender (Data Extraction)

This extracts user records from the API response and wraps them for the identity pipeline.

Key concepts:

  • The API response is available at parsed_response.value.
  • Use MAP() to iterate over the array of records.
  • Each record must be wrapped in a record.json structure.
  • Inside the MAP(), each array element is referenced as item.
  • For response keys containing dots (for example, user.email), use bracket notation: item["user.email"].
  • For regular keys (for example, employee_number), use dot notation: item.employee_number.

Example Output Bender for the ServiceNow response above:

1{
2 "MAP()": {
3 "items": "parsed_response.value.result",
4 "converter": {
5 "record": {
6 "json": {
7 "email_addr": "item[\"user.email\"].value",
8 "sys_id": "item[\"user.sys_id\"].value",
9 "employment_start_date": "item.employment_start_date.value",
10 "employee_number": "item.employee_number.value",
11 "user_name": "item[\"user.user_name\"].value"
12 }
13 }
14 }
15 }
16}

Example Output Bender if no field filtering is needed:

1{
2 "MAP()": {
3 "items": "parsed_response.value.value",
4 "converter": {
5 "record": {
6 "json": "item"
7 }
8 }
9 }
10}

Breaking this down:

  • "items": "parsed_response.value.result" — points to the array of user records in the API response.
  • "converter" — defines how each record is transformed.
  • "record": { "json": { ... } }required wrapper that packages each record for the identity pipeline.
  • item["user.email"].value — for each record, extracts the value field from the user.email object.
  • item.employee_number.value — same idea for keys without dots in the name.

Common Mistakes:

  • Missing the record.json wrapper — records will not be processed without it.
  • Using $["key"] instead of ["key"] for bracket access on nested objects — $["key"] is only for root-level access.
  • Using parsed_response.json.result instead of parsed_response.value.result — the HTTP Action crawler uses the value parser, not json.

Next Request Details (Pagination)

This controls whether the crawler fetches additional pages of results.

Execution Condition — a DSL rule that determines if there is a next page. The crawler stops when this evaluates to false:

1$LENGTH(parsed_response.value.result) > 0

This continues paginating as long as the current response returned results. When the API returns an empty array, pagination stops.

Next Request Action — select the same HTTP Action (for example, get_hr_profile).

Next Request Input Args — computes the next page’s parameters based on the previous crawler request:

1// pagination via "cursor", where the API "skips" based on a limit
2{
3 "sysparm_offset": "$TEXT($INTEGER(crawler_request.request.params.sysparm_offset) + $INTEGER(crawler_request.request.params.sysparm_limit))",
4 "sysparm_limit": "crawler_request.request.params.sysparm_limit"
5}
6
7// pagination via "page", where the API returns pages of users with a "next" parameter
8{
9 "page": "$TEXT($INTEGER(crawler_request.request.params.page) + $INTEGER('1'))",
10 "page_size": "'1000'"
11}

Key concepts:

  • The query parameters you set up in the HTTP Action are accessible via crawler_request.request.
  • How you paginate depends on your API. Some APIs follow an “offset” pattern, where the API returns users based on where it left off; others have a configurable page size with page numbers you can key off of.

Special Scenario

If your query parameter starts with $, you need to handle the dot walk in a special manner. For example, to dot-walk to a query param named $skip:

1{
2 "skip": "$TEXT($INTEGER($['crawler_request']['request']['params']['$skip']) + 1200)",
3 "top": "1200"
4}

Breaking this down:

  • crawler_request.request — the previous request’s input args (params, headers, body, path, method).
  • $INTEGER(...) — converts a string to an integer (needed for arithmetic).
  • + — adds the offset and limit to compute the next offset.
  • $TEXT(...) — converts the result back to a string (query params are strings).
  • The limit is carried forward unchanged from the previous request.

DSL Function Reference:

  • All DSL functions use a $ prefix: $LENGTH(), $TEXT(), $INTEGER(), etc.
  • Arithmetic uses infix operators: +, -, *, /.
  • String literals use single quotes: 'hello'.
  • Use eval blocks when you need DSL expressions inside a data mapping (Bender) context.

Step 3: Configure the Identity Ingestion Source

Once the Crawl Config is set up, configure the Identity Ingestion to map the extracted data to the Moveworks User Profile.

3a. Add the Source

In the Identity Ingestion Configuration, add a new source:

  • Integration ID — select the same Integration used in the Crawl Config and HTTP Action.
  • Is Primary Source — set to true if this is your main identity source (exactly one source must be primary).

3b. Source-Specific User Attribute Mapping

This maps the fields from the Crawl Config output to Moveworks User Profile fields.

The input to this mapping is the data you defined inside record.json in the Output Bender. Each field is available at the root level — no prefix needed.

Example:

1{
2 "email_addr": "email_addr",
3 "record_id": "sys_id",
4 "employee_id": "employee_number",
5 "login": "user_name",
6 "employee_start_date": "employment_start_date"
7}

How to read this mapping:

  • Left side = Moveworks User Profile field name.
  • Right side = field name from your Output Bender’s record.json object.
  • Example: "record_id": "sys_id" means “take the sys_id value from the crawled record and store it as record_id in the Moveworks User Profile.”

Important:

  • Do NOT use record. or item. prefixes — the fields are already at the root level when this mapping runs.
  • The field names on the right must exactly match the keys you defined in the Output Bender’s record.json.

3c. Joining Key

Set the Joining Key to the field that uniquely identifies users across sources. This is typically email_addr.

If you have multiple identity sources, the Joining Key is used to match and merge user records across sources.

3d. Merge Bender (Multi-Source Only)

If this is your only identity source, leave this empty.

If you have multiple sources, use the Merge Bender to override specific fields from alternate sources. Fields from the primary source are included automatically.

Step 4: Verify the Configuration

After saving all configuration, verify the following:

  • The HTTP Action’s Integration ID matches the Integration.
  • The Crawl Config’s Integration ID matches the same Integration.
  • The Identity Ingestion Source’s Integration ID matches the same Integration.
  • Exactly one source is marked as primary_source = true.
  • The Joining Key is set and the corresponding field is mapped in the Source Attribute Mapping.

End-to-End Example: ServiceNow HR Profile

Here is a complete configuration example using ServiceNow as the identity source.

HTTP Action: get_hr_profile

SettingValue
ConnectorYour ServiceNow HTTP connector
MethodGET
Input Argssysparm_offset (query param), sysparm_limit (query param)

Crawl Config

Start Request Input Args:

1{
2 "sysparm_offset": "'0'",
3 "sysparm_limit": "'100'"
4}

Output Bender:

1{
2 "MAP()": {
3 "items": "parsed_response.value.result",
4 "converter": {
5 "record": {
6 "json": {
7 "email_addr": "item[\"user.email\"].value",
8 "sys_id": "item[\"user.sys_id\"].value",
9 "employment_start_date": "item.employment_start_date.value",
10 "employee_number": "item.employee_number.value",
11 "user_name": "item[\"user.user_name\"].value"
12 }
13 }
14 }
15 }
16}

Execution Condition:

1$LENGTH(parsed_response.value.result) > 0

Next Request Input Args:

1{
2 "sysparm_offset": {
3 "eval": {
4 "expression": "$TEXT($INTEGER(crawler_request.request.params.sysparm_offset) + $INTEGER(crawler_request.request.params.sysparm_limit))"
5 }
6 },
7 "sysparm_limit": "crawler_request.request.params.sysparm_limit"
8}

Identity Ingestion Source

Source Attribute Mapping:

1{
2 "email_addr": "email_addr",
3 "record_id": "sys_id",
4 "employee_id": "employee_number",
5 "login": "user_name",
6 "employee_start_date": "employment_start_date"
7}

Joining Key: email_addr

Primary Source: true

Quick Reference: Data Context at Each Stage

Understanding what data is available at each configuration stage is critical for writing correct expressions.

Configuration FieldAvailable ContextExample Access
Start Request Input ArgsEmpty (no prior data)"'0'" (string literals only)
Output Benderparsed_response.value (API response), crawler_request.request (request details)parsed_response.value.result
Execution ConditionSame as Output Bender$LENGTH(parsed_response.value.result) > 0
Next Request Input Argscrawler_request.request — the previous request’s input args (params, headers, body, path, method)crawler_request.request.params.sysparm_offset
Source Attribute MappingRoot-level fields from record.json in the Output Benderemail_addr, sys_id

Troubleshooting

You are using $["key"] syntax on a nested object. The $ prefix for bracket access is only valid at the root level. For nested objects, use plain bracket notation:

  • Wrong: item.$["user.email"].value
  • Correct: item["user.email"].value

Check these common causes:

  1. Wrong response path — Ensure items in the Output Bender points to the correct array. For HTTP Actions, the path is parsed_response.value.<your_array_key>, NOT parsed_response.json.<your_array_key> or response.<your_array_key>.
  2. Execution condition error — If the execution condition references an invalid path (for example, response.result instead of parsed_response.value.result), it can cause the entire response handler to fail, preventing any records from being extracted.
  3. Missing record.json wrapper — The Output Bender converter must wrap fields inside { "record": { "json": { ... } } }. Without this wrapper, records will not be processed.

Verify the Next Request Input Args use crawler_request.request to access the input args from the previous request. Common mistakes:

  • Wrong: previous_action.sysparm_offset or response.sysparm_offset
  • Correct: crawler_request.request.params.sysparm_offset

The Source Attribute Mapping receives data at the root level. Do not use prefixes:

  • Wrong: record.email_addr or item.email_addr
  • Correct: email_addr

The right-side field names must exactly match the keys you defined in the Output Bender’s record.json.

All DSL functions require a $ prefix. Common corrections:

  • Wrong: LENGTH(), TEXT(), INTEGER()
  • Correct: $LENGTH(), $TEXT(), $INTEGER()

Arithmetic uses infix operators (+, -), not functions:

  • Wrong: $ADD(a, b)
  • Correct: a + b