Skip to content

feat(evo-flow): execute Move to Pipeline Stage Journey node (EVO-1272)#37

Merged
dpaes merged 1 commit into
developfrom
feat/EVO-1272
Jun 10, 2026
Merged

feat(evo-flow): execute Move to Pipeline Stage Journey node (EVO-1272)#37
dpaes merged 1 commit into
developfrom
feat/EVO-1272

Conversation

@nickoliveira23

@nickoliveira23 nickoliveira23 commented Jun 9, 2026

Copy link
Copy Markdown

What

Runtime for the Flow Builder Move to Pipeline Stage action node (EVO-1272).

  • MoveToPipelineStageNode mirrors AssignToPipelineNode; calls the CRM move_conversation endpoint via CrmClientService#moveToPipelineStage.
  • Unwraps the CRM success_response envelope (response.data.data) so a skip for a deleted target stage surfaces as skipped, not a phantom move.
  • Registers the node in action-nodes.activities (interface, lazy singleton, activity) and dispatches the move-to-pipeline-stage-node type in journey-execution.workflow.

Tests

  • Node unit specs: happy path, skip-on-missing-config, skip-on-deleted-stage (AC3), error.
  • CrmClientService contract spec pinning URL, method, body and the nested envelope against mocked fetch — this caught a real bug where the node read response.data.skipped instead of response.data.data.skipped (a deleted stage would have been reported as a successful move).

Known limitation (out of scope)

Journey trigger events that are contact-scoped (e.g. contact label add/remove) do not carry a conversation_id, so this node — like the existing Assign to Pipeline node — has no conversation to act on when started that way. Wiring conversation_id into Journey trigger context is Journey-infra (EVO-1634), tracked separately.

Part of EVO-1272 — paired with CRM (evo-ai-crm-community#129) and frontend PRs.

Summary by Sourcery

Add runtime support for a Move to Pipeline Stage journey node that moves a conversation between CRM pipeline stages and wires it into journey execution.

New Features:

  • Introduce a MoveToPipelineStageNode action node that moves a conversation to a configured CRM pipeline stage and reports move or skip outcomes.
  • Expose an executeMoveToPipelineStageNode Temporal activity and dispatch it from the journey execution workflow for the new node type.
  • Add a CrmClientService.moveToPipelineStage method to call the CRM move_conversation endpoint with the appropriate identifiers.

Tests:

  • Add unit tests for MoveToPipelineStageNode covering successful moves, configuration-based skips, deleted-stage skips, and error handling.
  • Add a contract test for CrmClientService.moveToPipelineStage to pin the HTTP method, URL, request body, and nested response envelope structure.

Implements the runtime for the Flow Builder "Move to Pipeline Stage" action node:

- MoveToPipelineStageNode mirrors AssignToPipelineNode; calls the CRM
  move_conversation endpoint via CrmClientService#moveToPipelineStage.
- Unwraps the success_response envelope (response.data.data) so a CRM skip for a
  deleted target stage surfaces as skipped, not a phantom move.
- Registers the node in action-nodes.activities (interface, lazy singleton,
  activity) and dispatches the 'move-to-pipeline-stage-node' type in the journey
  execution workflow.

Tests: node unit specs (happy / skip-on-missing-config / skip-on-deleted-stage /
error) and a CrmClientService contract spec pinning URL, method, body and the
nested envelope against fetch.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@sourcery-ai

sourcery-ai Bot commented Jun 9, 2026

Copy link
Copy Markdown

Reviewer's Guide

Implements the runtime for the Move to Pipeline Stage journey action node by adding a CRM client method, a Temporal activity node with proper envelope handling and skip semantics, and wiring it into the action-node activities and journey execution workflow, plus tests that pin the CRM HTTP contract and node behavior.

Sequence diagram for executing the Move to Pipeline Stage journey node

sequenceDiagram
  actor TriggerEvent
  participant JourneyExecutionWorkflow
  participant ActionNodeActivities
  participant MoveToPipelineStageNode
  participant CrmClientService
  participant CrmAPI

  TriggerEvent ->> JourneyExecutionWorkflow: JourneyExecutionWorkflow(input)
  JourneyExecutionWorkflow ->> JourneyExecutionWorkflow: switch currentNode.type
  JourneyExecutionWorkflow ->> ActionNodeActivities: executeMoveToPipelineStageNode(input)
  ActionNodeActivities ->> MoveToPipelineStageNode: execute(input)

  alt [missing stageId or pipelineId or conversationId]
    MoveToPipelineStageNode ->> MoveToPipelineStageNode: skipped(reason, pipelineId, stageId)
    MoveToPipelineStageNode -->> ActionNodeActivities: NodeExecutionResult (skipped)
  else [all IDs present]
    MoveToPipelineStageNode ->> CrmClientService: moveToPipelineStage(pipelineId, conversationId, stageId, nodeType)
    CrmClientService ->> CrmAPI: PATCH /pipelines/{pipelineId}/pipeline_items/move_conversation
    CrmAPI -->> CrmClientService: { success, data: { data: MoveResponseData } }
    CrmClientService -->> MoveToPipelineStageNode: CrmApiResponse

    alt [response.success is false]
      MoveToPipelineStageNode ->> MoveToPipelineStageNode: throw Error
      MoveToPipelineStageNode -->> ActionNodeActivities: createErrorResult(error, executionTime)
    else [crmData.skipped is true]
      MoveToPipelineStageNode ->> MoveToPipelineStageNode: log warn "CRM skipped the move"
      MoveToPipelineStageNode -->> ActionNodeActivities: NodeExecutionResult (skipped)
    else [move succeeded]
      MoveToPipelineStageNode -->> ActionNodeActivities: NodeExecutionResult (moved)
    end
  end

  ActionNodeActivities -->> JourneyExecutionWorkflow: nodeResult
  JourneyExecutionWorkflow ->> JourneyExecutionWorkflow: continue journey graph
Loading

File-Level Changes

Change Details Files
Add CrmClientService support for moving a conversation to a pipeline stage and pin its HTTP contract.
  • Introduce moveToPipelineStage method that PATCHes the pipeline_items/move_conversation endpoint with conversation_id and pipeline_stage_id and passes nodeType/conversationId for logging/metadata.
  • Verify via a new spec that the method hits the correct URL, uses PATCH, sends the expected JSON body, and preserves the nested { success, data } envelope in the returned value.
src/shared/crm-client/crm-client.service.ts
src/shared/crm-client/crm-client.service.spec.ts
Introduce MoveToPipelineStageNode Temporal activity that calls CRM, unwraps the success_response envelope, and surfaces skipped moves correctly.
  • Create MoveToPipelineStageNode class extending BaseNode with lazy CrmClientService creation, handling multiple pipeline/stage ID field names in nodeData, and skipping when configuration or conversationId is missing.
  • Call CrmClientService.moveToPipelineStage and unwrap response.data.data, treating crmData.skipped as a skip (with logging) and returning structured move/skip metadata; on errors, log and surface an error NodeExecutionResult via createErrorResult.
  • Add a helper skipped() method to normalize skip results and feed them into the BaseNode timing/success wrapper for consistent metrics and outputs.
src/modules/temporal/activities/nodes/evoai/pipeline/move-to-pipeline-stage.node.ts
Wire MoveToPipelineStageNode into Temporal action node activities and the JourneyExecutionWorkflow.
  • Extend ActionNodeActivities interface and implementation with executeMoveToPipelineStageNode, including a lazy singleton getter for the new node.
  • Expose MoveToPipelineStageNodeInput in the action-nodes.activities barrel exports so workflows can type their inputs.
  • Update JourneyExecutionWorkflow to handle the 'move-to-pipeline-stage-node' type by constructing the node input (nodeId, conversationId from triggerEvent properties, sessionId, nodeData) and delegating to executeMoveToPipelineStageNode.
src/modules/temporal/activities/action-nodes.activities.ts
src/modules/temporal/workflows/journey-execution.workflow.ts
src/modules/temporal/activities/nodes/evoai/pipeline/move-to-pipeline-stage.node.spec.ts

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Hey - I've found 1 issue, and left some high level feedback:

  • In MoveToPipelineStageNode.execute, the success path uses executeWithTiming’s executionTime while the catch branch sets executionTime = Date.now(), which likely mixes up a duration with an absolute timestamp; consider using the same timing mechanism in both paths for consistent metrics.
  • The skipped helper in MoveToPipelineStageNode does not include conversationId while the CRM-skipped path does, which makes downstream handling and debugging inconsistent; consider adding conversationId to the helper signature and including it in all skipped results.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- In `MoveToPipelineStageNode.execute`, the success path uses `executeWithTiming`’s `executionTime` while the `catch` branch sets `executionTime = Date.now()`, which likely mixes up a duration with an absolute timestamp; consider using the same timing mechanism in both paths for consistent metrics.
- The `skipped` helper in `MoveToPipelineStageNode` does not include `conversationId` while the CRM-skipped path does, which makes downstream handling and debugging inconsistent; consider adding `conversationId` to the helper signature and including it in all skipped results.

## Individual Comments

### Comment 1
<location path="src/modules/temporal/activities/nodes/evoai/pipeline/move-to-pipeline-stage.node.ts" line_range="109-117" />
<code_context>
+        timestamp: new Date().toISOString(),
+        crmResponse: crmData,
+      };
+    })
+      .then(({ result, executionTime }) => {
+        return this.createSuccessResult(input, executionTime, {
+          [`node_${input.nodeId}_pipeline_moved`]: result.moved,
+          [`node_${input.nodeId}_pipeline_id`]: result.pipelineId,
+          [`node_${input.nodeId}_stage_id`]: result.stageId,
+        });
+      })
+      .catch((error) => {
+        const executionTime = Date.now();
+        this.logger.error('Failed to move conversation to pipeline stage', {
</code_context>
<issue_to_address>
**issue (bug_risk):** Error path uses `Date.now()` for `executionTime`, which likely diverges from the timing semantics used in the success path.

The success path gets `executionTime` as a duration from `executeWithTiming`, but the error path uses `Date.now()` and passes that into `createErrorResult`. If `executionTime` is meant to be a duration, this will distort error metrics and make them inconsistent with success timings. Please either route errors through `executeWithTiming` (if possible) or measure a duration around the call (e.g. `const start = Date.now(); ... catch { const executionTime = Date.now() - start; }`) so both paths use the same timing semantics.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment on lines +109 to +117
})
.then(({ result, executionTime }) => {
return this.createSuccessResult(input, executionTime, {
[`node_${input.nodeId}_pipeline_moved`]: result.moved,
[`node_${input.nodeId}_pipeline_id`]: result.pipelineId,
[`node_${input.nodeId}_stage_id`]: result.stageId,
});
})
.catch((error) => {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

issue (bug_risk): Error path uses Date.now() for executionTime, which likely diverges from the timing semantics used in the success path.

The success path gets executionTime as a duration from executeWithTiming, but the error path uses Date.now() and passes that into createErrorResult. If executionTime is meant to be a duration, this will distort error metrics and make them inconsistent with success timings. Please either route errors through executeWithTiming (if possible) or measure a duration around the call (e.g. const start = Date.now(); ... catch { const executionTime = Date.now() - start; }) so both paths use the same timing semantics.

@dpaes dpaes left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

✅ Aprovado — EVO-1272 [10.14] Move to Pipeline Stage

Revisão dos 3 PRs (crm#129, evo-flow#37, frontend#148) concluída. Contrato cross-repo coerente ponta-a-ponta; o contract spec do evo-flow pegou e corrigiu o bug de envelope (data.skippeddata.data.skipped).

Pendências resolvidas:

  • Rebase: crm#129 rebaseado na develop, agora MERGEABLE/CLEAN.
  • AC4: confirmado sem desvio — a paridade é medida contra Pipelines::StageAutomationService#move_to_pipeline, que faz o mesmo update! in-place deixando o pipeline anterior. AC1–AC4 atendidos.

Specs auto-reportados (CI não roda rspec/vitest) — recomendado rodar local. Aprovado e mergeado para develop.

@dpaes dpaes merged commit 32039e1 into develop Jun 10, 2026
2 checks passed
@dpaes dpaes deleted the feat/EVO-1272 branch June 10, 2026 00:52
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants