Join ExecModule
Overview
JoinModule version 2.0.0 performs deterministic inner, left, right, and full joins over two arrays of workflow objects. It is a local, pure data transformation: it needs no network access or IntegrationAccount, and it creates no external side effects.
Version 2 replaces catalog metadata with zero inputs and malformed output names, and hardens execution around explicit join and collision modes, nested key paths, fail-closed record validation, SQL-like null-key behavior, deterministic ordering, input limits, and a caller-bounded result limit.
Usage
- Put both arrays in workflow state.
- Configure
left_sourceandright_sourceas nested paths to those arrays. - Select the key path inside each record and the intended join type.
- Add prefixes when both sides can contain the same field names, or choose an explicit collision strategy.
- Set
max_output_recordsbelow the 100,000 hard cap when downstream work needs a tighter bound. - Read canonical rows from
items. The configuredoutput_fieldis also populated as a compatibility alias.
Inputs
| Name | Type | Required | Limit | Description |
|---|---|---|---|---|
left | array of objects | Yes | 10,000 records | Array resolved from left_source. |
right | array of objects | Yes | 10,000 records | Array resolved from right_source. |
The input names describe the logical sides of the join. Their physical locations are selected by the source-path configuration. Every array element must be an object; the module rejects the full execution rather than silently discarding invalid elements.
Outputs
| Name | Type | Condition | Description |
|---|---|---|---|
status | string | Always | success or error. |
items | array | Success | Canonical joined rows. |
joined_data | array | Success with default alias | Compatibility alias selected by default output_field. |
count | integer | Success | Number of output rows. |
leftCount | integer | Success | Validated left input count. |
rightCount | integer | Success | Validated right input count. |
matchedPairs | integer | Success | Number of actual left/right record pairs. |
joinType | string | Success | Normalized join mode. |
errorCode | string | Error | Stable failure code. |
error | string | Error | Safe, actionable failure message. |
When output_field is changed, the same result list is exposed under both items and that configured alias. Reserved status and diagnostic names cannot be used as aliases.
IntegrationAccount Requirements
None. JoinModule is an in-memory transformation and must not receive credentials. Joined data can be confidential, so downstream modules should preserve its classification and apply normal workflow ACL rules.
Configuration
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
left_source | text | Yes | — | Nested path to the left array, such as payload.users. |
right_source | text | Yes | — | Nested path to the right array, such as payload.orders. |
left_key | text | Yes | id | Nested key path inside left records. |
right_key | text | Yes | id | Nested key path inside right records. |
join_type | select | Yes | inner | inner, left, right, or full. |
collision_strategy | select | Yes | right_wins | right_wins, left_wins, or error. |
prefix_left | text | No | empty | Prefix applied to every left output field. |
prefix_right | text | No | empty | Prefix applied to every right output field. |
output_field | text | No | joined_data | Compatibility alias for canonical items. |
max_output_records | integer | No | 10000 | Execution-specific output cap, from 1 through 100,000. |
Paths support object segments and bounded zero-based array indexes, for example payload.groups[0].users or identity.id. Prefixes and aliases accept only bounded alphanumeric, underscore, dash, and dot characters.
Operations
JoinModule exposes one transformation with four modes:
| Mode | Matched rows | Unmatched left rows | Unmatched right rows |
|---|---|---|---|
inner | Included | Excluded | Excluded |
left | Included | Included | Excluded |
right | Included | Excluded | Included |
full | Included | Included | Included |
Duplicate keys produce one row for every matching pair. Null or missing keys never match; an outer join retains those records on their original side. Left input order is preserved, right-side matches preserve right input order, and unmatched right rows are appended in right input order.
Errors and Failure Modes
| Error code | Cause | Recovery |
|---|---|---|
VALIDATION_ERROR | Missing/unsafe path, unsupported join or collision mode, reserved alias, non-object record, or invalid limit. | Correct the named configuration or input. |
INPUT_LIMIT_EXCEEDED | Either source contains more than 10,000 records. | Partition the arrays or aggregate before joining. |
RESULT_LIMIT_EXCEEDED | The join would exceed max_output_records. | Tighten upstream filtering, use more selective keys, or raise the bound deliberately. |
FIELD_COLLISION | collision_strategy: error encountered the same output field on both sides. | Add distinct prefixes or choose an explicit winning side. |
EXECUTION_ERROR | An unexpected local runtime failure occurred. | Preserve the inputs, inspect sanitized runtime diagnostics, and retry only after fixing the defect. |
Failures clear partial rows and return only the safe error contract. Workflow data is not copied into logs or failure messages.
Example
Configuration:
{
"left_source": "payload.users",
"right_source": "payload.orders",
"left_key": "id",
"right_key": "user_id",
"join_type": "inner",
"prefix_left": "user_",
"prefix_right": "order_",
"collision_strategy": "error",
"max_output_records": 1000
}
Input:
{
"payload": {
"users": [
{"id": "u-1", "name": "Ada"},
{"id": "u-2", "name": "Lin"}
],
"orders": [
{"user_id": "u-1", "order_id": "o-7", "total": 42.5},
{"user_id": "u-1", "order_id": "o-8", "total": 19.0}
]
}
}
Expected result:
{
"status": "success",
"items": [
{
"user_id": "u-1",
"user_name": "Ada",
"order_user_id": "u-1",
"order_order_id": "o-7",
"order_total": 42.5
},
{
"user_id": "u-1",
"user_name": "Ada",
"order_user_id": "u-1",
"order_order_id": "o-8",
"order_total": 19.0
}
],
"count": 2,
"leftCount": 2,
"rightCount": 2,
"matchedPairs": 2,
"joinType": "inner"
}
The default joined_data alias contains the same rows as items.
Notes
- Pagination: none. Inputs are already-materialized workflow arrays; partition large datasets before this module.
- Limits: at most 10,000 records per side, nested paths up to 256 characters, prefixes up to 64 characters, aliases up to 128 characters, and at most 100,000 output rows.
- Idempotency: natural and deterministic for the same ordered inputs and configuration.
- Rate limits: none; the module does not call a provider.
- API constraints: key equality uses Java object equality after workflow deserialization. A numeric
7and string"7"are different keys. - Collision behavior:
right_winspreserves legacy overwrite behavior;left_winsretains the left field;errorfails the complete execution. Prefixes are the clearest contract. - Destructive behavior: none. No records are mutated, written externally, or deleted.
- Observability: events report only status, safe error code, and result count, never joined payloads.
- Testing boundary: focused tests cover join modes, one-to-many order, nested paths, null keys, collision policies, malformed records, result bounds, output compatibility, and metadata serialization. No external service is involved.
- Runtime boundary: merged source, production documentation, public launch content, and deployed
/v1/modules/metadataare separate states.
See the ExecModule catalog for neighboring Map, Merge, Filter, Sort, and Deduplicate transformations.