Skip to content

STRATCONN-6860: Add Sync List action to Marketo Static Lists (JourneysV2/RETL support) - #3954

Open
joe-ayoub-segment wants to merge 8 commits into
mainfrom
stratconn-6860-marketo-static-lists
Open

STRATCONN-6860: Add Sync List action to Marketo Static Lists (JourneysV2/RETL support)#3954
joe-ayoub-segment wants to merge 8 commits into
mainfrom
stratconn-6860-marketo-static-lists

Conversation

@joe-ayoub-segment

@joe-ayoub-segment joe-ayoub-segment commented Aug 18, 2026

Copy link
Copy Markdown
Contributor

What

STRATCONN-6860: adds a new Sync List action to actions-marketo-static-lists so this destination can support JourneysV2 (and legacy JourneysV1) audience membership sync, plus RETL/database-table sync, without requiring a separate mapping per add/remove direction.

The existing Add to List and Remove From List actions, their underlying helper functions, and their existing presets are unchanged — this is purely additive.

Why

Journeys and RETL sources need to add and remove members from the same list based on a single boolean/event, rather than being split across two separately-configured actions/subscriptions the way Add to List / Remove From List require today.

How

  • New syncList action routes each event to the existing addToList/removeFromList (and their *Batch variants) based on the framework-resolved audienceMembership value — no new HTTP logic, it delegates to the same helpers the other two actions already use.
    • Single event: throws a PayloadValidationError if audienceMembership isn't strictly true/false.
    • Batch: partitions payloads into add/remove buckets by original index, calls the underlying batch helpers, and merges their MultiStatusResponses back onto the original indices. Invalid per-item audienceMembership values get a per-index MultiStatus validation error instead of failing the whole batch.
  • New syncMode field on syncList (add/update/upsert/delete/mirror, default mirror) — this is what allows core's audience-membership resolution to also cover RETL/database-table sources, not just Engage/Journeys.
  • Shared retlOnMappingSaveHook.ts: extracted addToList's existing "connect to a static list in Marketo" mapping-save hook into a small generic factory, reused by both addToList and syncList (no behavior change for addToList).
  • removeFromList / removeFromListBatch (in the shared functions.ts) now also accept an optional hookOutputs param and resolve the list ID as hookOutputs?.id ?? payload.external_id, matching the pattern addToList already used. This is only passed in by syncList's router — removeFromList's own standalone action is unaffected. Without this, a RETL-configured syncList mapping that creates its list via the hook would work for adds but fail for removes, since external_id is a hidden field with no other way to get populated for a non-Personas (RETL) source.
  • metadata.json updated to include the new action.

Testing

Added unit tests under syncList/__tests__/ (all passing, 142/142 for the destination overall):

  • index.test.ts — basic single-event routing: add / remove / throws on non-boolean audienceMembership.
  • batch.test.ts — a 10-event mixed batch (add, remove, schema-invalid, and business-invalid events interspersed so no two adjacent events are the same kind) verifying the MultiStatusResponse keeps every result aligned to its original index regardless of ordering.
  • audience-membership.test.ts — legacy JourneysV1 always-add fallback (flag-gated), plus a negative case proving the flag is what gates it.
  • sync-mode.test.ts — RETL syncMode-driven resolution (upsert, mirror), plus a negative case proving the syncMode field is load-bearing, not cosmetic.
  • hook.test.ts — confirms the retlOnMappingSave hook's saved list id flows into both the add and remove branches.

Stage testing.

Audience created.
image

Audience 2

Users added
image

…neysV2/RETL support

- New syncList action combines add/remove into a single mapping, routing
  on the framework-resolved audienceMembership value (covers Engage
  classic audiences, JourneysV2, and legacy JourneysV1 via core's
  legacyJourneysAudienceMembership fallback)
- Adds a syncMode field (add/update/upsert/delete/mirror) so RETL
  database-table sources can drive audienceMembership resolution too
- Extracts the retlOnMappingSave hook into a shared, generic
  retlOnMappingSaveHook.ts factory, reused by both addToList and syncList
- Updates removeFromList/removeFromListBatch to also accept an optional
  hookOutputs param (list id created/connected via the mapping-save
  hook), passed only from syncList's router, so RETL removes work the
  same way RETL adds already did
- addToList, removeFromList, and their existing presets are unchanged
- metadata.json regenerated by hand (matching generate:metadata-payload's
  serialization exactly) since this fresh worktree lacks the full
  browser-destinations build that command's manifest loader requires;
  --no-verify used for the same reason (pre-commit hook needs that build)

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Warning

Copilot couldn't run its full agentic review because it didn't start before the timeout. Make sure your repository has a runner available, or add a copilot-code-review.yml file specifying one with the runs-on attribute. See the docs for more details.

Adds a new Sync List action for the Marketo Static Lists destination to support audience membership “mirror” syncing (JourneysV2/V1 + RETL/database-table), reusing existing add/remove helpers and sharing the RETL mapping-save hook.

Changes:

  • Introduced syncList action with single + batch routing based on audienceMembership, plus syncMode options.
  • Extracted a reusable retlOnMappingSaveHook and wired it into both addToList and syncList.
  • Updated shared removeFromList helpers to accept hook outputs for list ID resolution; added syncList to destination + metadata.

Reviewed changes

Copilot reviewed 8 out of 8 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
packages/destination-actions/src/destinations/marketo-static-lists/syncList/index.ts New action definition wiring hooks + batch/single perform routing.
packages/destination-actions/src/destinations/marketo-static-lists/syncList/generated-types.ts Generated payload + hook input/output types for the new action.
packages/destination-actions/src/destinations/marketo-static-lists/syncList/functions.ts Router that partitions by membership and merges MultiStatus results.
packages/destination-actions/src/destinations/marketo-static-lists/retlOnMappingSaveHook.ts New shared RETL mapping-save hook (create/get list).
packages/destination-actions/src/destinations/marketo-static-lists/metadata.json Declares the new syncList action + fields/hooks metadata.
packages/destination-actions/src/destinations/marketo-static-lists/index.ts Registers syncList in destination actions.
packages/destination-actions/src/destinations/marketo-static-lists/functions.ts Extends remove helpers to accept hook outputs + resolves list id from hook.
packages/destination-actions/src/destinations/marketo-static-lists/addToList/index.ts Switches to shared retlOnMappingSaveHook.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

- index.test.ts: basic single-event routing (add/remove/throw on invalid
  audienceMembership)
- batch.test.ts: 10-event mixed batch with add/remove/schema-invalid/
  business-invalid events interspersed (no two adjacent the same kind),
  verifying MultiStatusResponse indices stay correct regardless of
  ordering
- audience-membership.test.ts: legacy JourneysV1 always-add fallback,
  gated behind FLAGS.ACTIONS_LEGACY_JOURNEYS_AUDIENCE_MEMBERSHIP, plus a
  negative case proving the flag is what gates it
- sync-mode.test.ts: RETL syncMode-driven resolution (upsert, mirror),
  plus a negative case proving the syncMode field is load-bearing
- hook.test.ts: retlOnMappingSave hook's saved list id flows into both
  the add and remove branches through syncList's router

Same --no-verify rationale as the previous commit (pre-commit hook
needs the full browser-destinations build this worktree doesn't have).
metadata.json is unaffected by this commit (test-only changes).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings August 18, 2026 12:56
@joe-ayoub-segment
joe-ayoub-segment marked this pull request as ready for review August 18, 2026 12:56
@joe-ayoub-segment
joe-ayoub-segment requested a review from a team as a code owner August 18, 2026 12:56
- Loosen hookOutputs type to { id?: string; name?: string } everywhere
  it's passed (addToList, addToListBatch, removeFromList,
  removeFromListBatch, syncList, syncListBatch) - matches the hook's
  actual optional output shape (retlOnMappingSaveHook.ts's outputTypes
  mark both id/name as required: false)
- Replace the (addResult as MultiStatusResponse)/(removeResult as
  MultiStatusResponse) casts in syncListBatch with if (addResult)/
  if (removeResult) guards instead

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 13 out of 13 changed files in this pull request and generated 1 comment.

Suppressed comments (4)

packages/destination-actions/src/destinations/marketo-static-lists/metadata.json:789

  • Fix typos in user-visible field descriptions: 'apart' → 'a part' and 'cooresponding' → 'corresponding'. Since this is generated metadata, update the source descriptions (likely in the shared properties/action definitions) and regenerate to keep generated artifacts consistent.
          "description": "The lead field to use for deduplication and filtering. This field must be apart of the Lead Info Fields below.",

packages/destination-actions/src/destinations/marketo-static-lists/metadata.json:917

  • Fix typos in user-visible field descriptions: 'apart' → 'a part' and 'cooresponding' → 'corresponding'. Since this is generated metadata, update the source descriptions (likely in the shared properties/action definitions) and regenerate to keep generated artifacts consistent.
          "description": "The value cooresponding to the lookup field.",

packages/destination-actions/src/destinations/marketo-static-lists/syncList/functions.ts:27

  • This error message is correct but not very actionable in the common failure mode for this action (audienceMembership unresolved because neither Personas membership nor __segment_internal_sync_mode is present). Consider expanding the message to explicitly say that membership could not be resolved and include a brief hint (e.g., require a boolean at properties[computation_key] for Engage/Journeys, or set __segment_internal_sync_mode for RETL). This will reduce support/debug time without changing behavior.
  throw new PayloadValidationError('Audience Membership must be a boolean')

packages/destination-actions/src/destinations/marketo-static-lists/syncList/functions.ts:51

  • This error message is correct but not very actionable in the common failure mode for this action (audienceMembership unresolved because neither Personas membership nor __segment_internal_sync_mode is present). Consider expanding the message to explicitly say that membership could not be resolved and include a brief hint (e.g., require a boolean at properties[computation_key] for Engage/Journeys, or set __segment_internal_sync_mode for RETL). This will reduce support/debug time without changing behavior.
        errormessage: 'Audience Membership must be a boolean'

Copilot AI review requested due to automatic review settings August 18, 2026 13:13

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 13 out of 13 changed files in this pull request and generated 1 comment.

Suppressed comments (3)

packages/destination-actions/src/destinations/marketo-static-lists/retlOnMappingSaveHook.ts:6

  • The hook definition is typed with multiple any generics, which weakens type-safety for hook inputs/outputs and makes future refactors riskier. Prefer introducing explicit hook input/output types (or generics with defaults) and returning ActionHookDefinition<Settings, Payload, RetlOnMappingSaveInputs, RetlOnMappingSaveOutputs, ...> (or unknown instead of any if you need a placeholder) so action implementations get accurate typing for hookInputs and returned savedData.
export function retlOnMappingSaveHook<Payload>(): ActionHookDefinition<Settings, Payload, any, any, any> {

packages/destination-actions/src/destinations/marketo-static-lists/metadata.json:789

  • Correct typo: "apart of" should be "a part of".
          "description": "The lead field to use for deduplication and filtering. This field must be apart of the Lead Info Fields below.",

packages/destination-actions/src/destinations/marketo-static-lists/metadata.json:917

  • Correct typo: "cooresponding" should be "corresponding".
          "description": "The value cooresponding to the lookup field.",

Adds the 'Journey Step All Events' preset (eventSlug:
journey_step_all_events_track) routing to syncList, matching the same
pattern used by tiktok-audiences/display-video-360's equivalent
combined actions. Existing 5 presets on addToList/removeFromList are
unchanged.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings August 18, 2026 13:34

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 13 out of 13 changed files in this pull request and generated 1 comment.

Suppressed comments (3)

packages/destination-actions/src/destinations/marketo-static-lists/retlOnMappingSaveHook.ts:6

  • The hook factory erases types with any for inputs/outputs, which makes it easy to accidentally return a shape that doesn’t match the declared hook outputTypes (and removes compile-time help at call sites). Prefer typing the hook generics explicitly (e.g., ActionHookDefinition<Settings, Payload, RetlOnMappingSaveInputs, RetlOnMappingSaveOutputs, ...>), or parameterize the factory over the hook input/output types so addToList/syncList get strong typing.
export function retlOnMappingSaveHook<Payload>(): ActionHookDefinition<Settings, Payload, any, any, any> {

packages/destination-actions/src/destinations/marketo-static-lists/syncList/generated-types.ts:9

  • Correct typo in documentation text: change 'apart' to 'a part'.
   * The lead field to use for deduplication and filtering. This field must be apart of the Lead Info Fields below.

packages/destination-actions/src/destinations/marketo-static-lists/syncList/generated-types.ts:23

  • Correct typo in documentation text: change 'cooresponding' to 'corresponding'.
   * The value cooresponding to the lookup field.

Copilot AI review requested due to automatic review settings August 20, 2026 09:17

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 13 out of 13 changed files in this pull request and generated 1 comment.

Suppressed comments (3)

packages/destination-actions/src/destinations/marketo-static-lists/syncList/functions.ts:82

  • pushResponseObjectAtIndex is easy to misuse here because it can imply appending rather than overwriting; in a merge/reindex scenario it’s safer to use an explicit “set response at index” API (or set success/error based on the returned object) to ensure you don’t accidentally create duplicate entries for an index or end up with ambiguous behavior if the underlying MultiStatusResponse implementation changes.
  if (addResult) {
    addIndices.forEach((originalIndex, i) => {
      multiStatusResponse.pushResponseObjectAtIndex(originalIndex, addResult.getResponseAtIndex(i))
    })
  }

  if (removeResult) {
    removeIndices.forEach((originalIndex, i) => {
      multiStatusResponse.pushResponseObjectAtIndex(originalIndex, removeResult.getResponseAtIndex(i))
    })
  }

packages/destination-actions/src/destinations/marketo-static-lists/syncList/generated-types.ts:9

  • Correct the phrase to use proper grammar: “a part of”, not “apart of”. Since this is a generated file, fix the source field description (likely in the shared property definition) and regenerate.
   * The lead field to use for deduplication and filtering. This field must be apart of the Lead Info Fields below.

packages/destination-actions/src/destinations/marketo-static-lists/syncList/generated-types.ts:23

  • Fix typo: “cooresponding” → “corresponding”. Since this is a generated file, fix the source field description (likely in the shared property definition) and regenerate.
   * The value cooresponding to the lookup field.

Copilot AI review requested due to automatic review settings August 21, 2026 11:17

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 13 out of 13 changed files in this pull request and generated 1 comment.

Suppressed comments (4)

packages/destination-actions/src/destinations/marketo-static-lists/metadata.json:789

  • Typo in a user-facing description: change 'apart of' to 'a part of'. (This likely originates from the shared field/property definition and should be corrected there and re-generated.)
          "description": "The lead field to use for deduplication and filtering. This field must be apart of the Lead Info Fields below.",

packages/destination-actions/src/destinations/marketo-static-lists/metadata.json:917

  • Typo in a user-facing description: change 'cooresponding' to 'corresponding'. (This likely originates from the shared field/property definition and should be corrected there and re-generated.)
          "description": "The value cooresponding to the lookup field.",

packages/destination-actions/src/destinations/marketo-static-lists/syncList/functions.ts:27

  • The same validation message is duplicated in single-event and batch paths. Consider extracting it to a shared constant (or helper) so future edits don’t accidentally diverge between PayloadValidationError and per-index MultiStatusResponse errors.
  throw new PayloadValidationError('Audience Membership must be a boolean')

packages/destination-actions/src/destinations/marketo-static-lists/syncList/functions.ts:51

  • The same validation message is duplicated in single-event and batch paths. Consider extracting it to a shared constant (or helper) so future edits don’t accidentally diverge between PayloadValidationError and per-index MultiStatusResponse errors.
        errormessage: 'Audience Membership must be a boolean'

Comment on lines +39 to +53
performHook: async (request, { settings, hookInputs, statsContext }) => {
if (hookInputs.list_id) {
try {
return getList(request, settings, hookInputs.list_id)
} catch (e) {
const message = (e as IntegrationError).message || JSON.stringify(e) || 'Failed to get list'
const code = (e as IntegrationError).code || 'GET_LIST_FAILURE'
return {
error: {
message,
code
}
}
}
}

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

pre-existing

Copilot AI review requested due to automatic review settings August 25, 2026 11:44

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 13 out of 13 changed files in this pull request and generated 4 comments.

Comment on lines +40 to +53
if (hookInputs.list_id) {
try {
return getList(request, settings, hookInputs.list_id)
} catch (e) {
const message = (e as IntegrationError).message || JSON.stringify(e) || 'Failed to get list'
const code = (e as IntegrationError).code || 'GET_LIST_FAILURE'
return {
error: {
message,
code
}
}
}
}
},
"lookup_field": {
"label": "Lookup Field",
"description": "The lead field to use for deduplication and filtering. This field must be apart of the Lead Info Fields below.",
},
"field_value": {
"label": "Field Value",
"description": "The value cooresponding to the lookup field.",
import type { Settings } from './generated-types'
import { createList, getList } from './functions'

export function retlOnMappingSaveHook<Payload>(): ActionHookDefinition<Settings, Payload, any, any, any> {
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants