Skip to content

fix(workflows,tasks): approve/reject condition scheme, Task V2 migration & resolve, v1122 workflow-handler init, TagUpdate version bump#29931

Merged
pmbrull merged 29 commits into
mainfrom
fix/user-approval-condition-scheme
Jul 20, 2026
Merged

fix(workflows,tasks): approve/reject condition scheme, Task V2 migration & resolve, v1122 workflow-handler init, TagUpdate version bump#29931
pmbrull merged 29 commits into
mainfrom
fix/user-approval-condition-scheme

Conversation

@yan-3005

@yan-3005 yan-3005 commented Jul 10, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Task V2 seeds use approve/reject on userApprovalTask outbound edges, but v1105 migration and runtime paths were still emitting legacy true/false, producing duplicate edges + stuck resolves.
  • Standardize on approve/reject across backend + UI + tests, with a BPMN-inspection safety layer so pre-migration Flowable process instances keep working.
  • Restore CreateApprovalTaskImpl as a deprecated shim so any frozen pre-Task-V2 ProcessDefinition still resolves the class it references.

Backend

  • New shared constants in Workflow: APPROVE_CONDITION / REJECT_CONDITION / LEGACY_APPROVE_CONDITION / LEGACY_REJECT_CONDITION.
  • v1105 MigrationUtil now writes approve/reject on ApprovalForUpdates outbound edges.
  • v200 MigrationUtil.TaskWorkflow.runRecognizerFeedbackTaskTypeMigration runs a one-shot rewrite that normalizes userApprovalTask outbound edges to approve/reject and dedups leftover duplicates from prior v1105 runs.
  • TaskWorkflowHandler.resolveWorkflowResult now inspects the deployed BPMN first via a new WorkflowHandler.getExpectedResultForActiveTask helper, so running Flowable instances get whichever condition string their frozen BPMN expects.
  • WorkflowHandler.handleMultiApproval and AutoApproveServiceTaskImpl.resolvePositiveResult write the deployed BPMN's expected condition string.
  • Removed the legacy RecognizerFeedbackApproval/DataQualityReview carveout in TaskWorkflowHandler — the BPMN-inspection fallback covers both.
  • WorkflowDefinitionRepository.validateApprovalConditions accepts approve/reject (plus legacy true/false) on userApprovalTask outbound edges. Non-approval conditional nodes still require true/false.
  • RecognizerFeedbackReviewWorkflow.json seed uses approve/reject.
  • Restored CreateApprovalTaskImpl as an empty @Deprecated(forRemoval = true, since = "2.0") extends CreateTask shim so pre-Task-V2 ProcessDefinitions whose BPMN references the historical FQN don't hit ClassNotFoundException.

UI

  • WorkflowBuilder.constants.ts adds ConditionValue.APPROVE/.REJECT and AVAILABLE_OPTIONS.APPROVAL_CONDITION_VALUES.
  • ConnectionConditionModal.tsx offers Approve/Reject when the source node is userApprovalTask (other conditional nodes keep True/False).
  • WorkflowSerializer.ts renders approve in green and reject in red.

Tests

  • WorkflowDefinitionResourceIT — 29 approval outbound edges rewritten from true/false to approve/reject; corresponding validation assertion updated.
  • v1105/MigrationUtilTest — migrated fixture + assertion updated.
  • WorkflowOssRestrictions.spec.ts — 4 approval outbound edges rewritten.

Test plan

  • mvn -pl openmetadata-service compile
  • mvn -pl openmetadata-service test-compile
  • mvn -pl openmetadata-service spotless:check
  • mvn -pl openmetadata-service test -Dtest='MigrationUtil*,TaskWorkflow*Test,WorkflowHandler*Test' — 169 tests, 0 failures
  • mvn -pl openmetadata-integration-tests test -Dtest='WorkflowDefinitionResourceIT' — 53 tests, 0 failures, 9 pre-existing @disabled skips
  • Manual verify auto-approval + multi-approval on 2.0
  • Manual verify pre-Task-V2 running instances still resolve after upgrade

Follow-up in this PR — v200 open-task + suggestion migration + resolve

Closes #30134. Additional fixes covering the migration + resolve path for open tasks and suggestions, surfaced end-to-end while validating the approve/reject work. Each landed as its own commit so bisect stays useful.

Migration + runtime resolution

  • WorkflowHandler.getRuntimeWorkflowInstanceId falls back to workflowInstanceExecutionIdworkflow_instance_state_time_series when the task-scoped workflowInstanceId Flowable variable is missing (pre-2.0 umbrellas never wrote it).
  • v200 SQL migration (migrateThreadTasksToTaskEntity) sets the umbrella workflowInstanceId on every migrated Task V2 row via a shared static helper. Closes the shouldCreateWorkflowManagedTask gate so postCreate → triggerWorkflowManagedTask does not spawn a duplicate task-scoped subprocess. The 1.13 umbrella keeps ownership of the Flowable userTask; the resolve endpoint completes it, the umbrella advances, and the History card reaches FINISHED.
  • v200 SQL migration resolves about.id by FQN via EntityRepository.getByName when the source thread / suggestion JSON lacks it. The MENTIONED_IN entity_relationship row now lands so the Task V2 resolve endpoint can patch the target entity (previously silent no-op on migrated Suggestion / DescriptionUpdate / TagUpdate).

Tag / suggestion resolve

  • SuggestTagLabel migration writes payload.fieldPath = "tags" explicitly. Without this the tag JSON was migrated with fieldPath = "description" because extractFieldPathFromEntityLink defaults there for entity-level entity links.
  • TaskWorkflowHandler.resolveTagTargetFqn returns the entity FQN (not <entityFQN>.tags) when fieldPath is "tags" or ends in .tags. Previously applyTags wrote a tag_usage row keyed on <entityFQN>.tags which no reader ever joined against — the entity read back with tags: [].

Read-path field defaults

  • TaskResource list endpoints default fields= to a narrow LIST_FIELDS = "assignees,about,createdBy". Enough for the UI card (fixes the "No Owners" render, since the UI wasn't passing fields= explicitly); no per-row hydration of the heavier comments / payload / domains / watchers relationships. Single-task GETs (/tasks/{id}, /tasks/name/{fqn}) still default to the full FIELDS set — one task, ~8 relationship queries, not a page-hydration concern.

Perf

  • Batched the ACT_HI_VARINST → workflow_instance_state_time_series umbrella lookup: one join per migration page instead of two per row. Consolidated the per-row helper into lookupLegacyUmbrellaWorkflowInstanceIds(Handle, Collection<String>); both the raw-SQL migration and the Task V2 cutover pre-collect thread ids and use the batched form. Prevents the CLI JDBI handle from holding for hours on catalogs with thousands of pre-2.0 open tasks.
  • Added a covering index on workflow_instance_state_time_series(workflowInstanceExecutionId) in v200 schemaChanges.sql (MySQL guarded via information_schema.statistics since MySQL 8.0 has no CREATE INDEX IF NOT EXISTS; Postgres via CREATE INDEX IF NOT EXISTS) so both the batched migration join and the runtime executionId fallback avoid full-scanning the state series.

Verification — fresh 1.13 → 2.0 upgrade with a mixed data set (open GlossaryApproval, closed GlossaryApproval, DescriptionUpdate, TagUpdate, SuggestDescription, SuggestTagLabel):

Task type Before After
Open GlossaryApproval umbrella zombie, History card stuck RUNNING umbrella advances → term Approved, WI FINISHED, card 9/9 stages FINISHED
SuggestDescription task Approved, description unchanged description patched with suggested value
DescriptionUpdate task Approved, description unchanged description patched with new value
SuggestTagLabel task Approved, tags: [] tag applied to entity
TagUpdate task Approved, tags: [] tag applied to entity
Any GET /v1/tasks[/{id}] assignees: [], about: null on default projection populated on default projection

See #30134 for the full failure-mode summary.


Follow-up commits after initial review

fix(migration,v1122) — every other version-migration that touches workflow definitions (v1105, v170, v171, v190, v11212, v1131, v200) calls initializeWorkflowHandler() at the top of runDataMigration(). v1122 was the only miss — it wrapped MigrationUtil.migrateWorkflowDefinitions() in a defensive try/catch instead. migrateWorkflowDefinitions calls repository.createOrUpdate on each workflow def, which triggers WorkflowDefinitionRepository.postUpdateWorkflowHandler.getInstance().deploy(...). Without initialization, getInstance() throws UnhandledServerException("WorkflowHandler is not initialized.") — the outer try/catch swallows it and the migration reports SUCCESS with zero definitions actually redeployed. Workflow-backed features (Knowledge Page approvals, DAR, Recognizer feedback, DQ incidents, custom workflows) may then run on stale definitions until a server restart re-initializes and redeploys them. Fix adds initializeWorkflowHandler() on both mysql/v1122 and postgres/v1122, keeps the try/catch as belt-and-suspenders.

fix(tasks) — entity version bump on TagUpdate resolve — resolving a TagUpdate task on 2.0 applied the tag to tag_usage but left the entity version, changeDescription, and taskResolved change event stale (regressed vs 1.13 where a task-driven tag write bumped the entity minor version). TaskWorkflowHandler.applyMergeTagsAction wrote directly to tag_usage via EntityRepository.applyTags/applyTagsDelete, sidestepping the versioned entity update path. Entity-level tag tasks now snapshot entity JSON, mutate entity.tags in-memory (union-add / remove / dedupe by tagFQN), diff, and apply via repository.patch — the same code path a user-driven PATCH /v1/tables/{id} on tags takes. Version bumps once, change event fires, tag_usage syncs. Column/field-level tag tasks stay on the direct DAO write: reflection-based mutation on nested Column objects produces a JSON patch that storeEntity flags as entityChanged=false (nested tag lists round-trip differently through Jackson) and silently drops the tag update, so the DAO path preserves existing tag-write behavior there (tags apply, no bump — pre-existing gap not in scope).

Task type Before After
Entity-level TagUpdate resolve tags applied, version stays 0.1 tags applied, version 0.1 → 0.2, change event fires
Column-level TagUpdate resolve tags applied, no version bump unchanged (pre-existing behavior)
TagUpdate close (reject) no write no write (unchanged)

Verified 2.0 fresh and 1.13 → 2.0 migrated on MySQL.

Greptile Summary

This PR updates approval workflow handling and Task V2 migration paths. The main changes are:

  • Standardized approval branches on approve and reject.
  • Added runtime compatibility for legacy Flowable process definitions.
  • Migrated open task and suggestion data into the Task V2 shape.
  • Fixed migrated tag and suggestion resolution paths.
  • Added task list field defaults and workflow lookup indexes.
  • Updated workflow builder UI options and serializer colors.

Confidence Score: 5/5

This looks safe to merge from the scoped follow-up review.

  • No new blocking issue cleared the follow-up review bar.
  • The remaining approval-casing concern matches an already reported thread rather than a distinct new fix requirement.

Important Files Changed

Filename Overview
openmetadata-ui/src/main/resources/ui/src/constants/WorkflowBuilder.constants.ts Approval condition constants now use lowercase backend values.
openmetadata-ui/src/main/resources/ui/src/components/WorkflowDefinitions/WorkflowBuilder/ConnectionConditionModal.tsx The condition modal now switches approval tasks to approval-specific options.
openmetadata-ui/src/main/resources/ui/src/utils/WorkflowSerializer.ts The serializer now renders approval and reject edges with dedicated labels and colors.
openmetadata-service/src/main/java/org/openmetadata/service/governance/workflows/WorkflowHandler.java Workflow resolution now reads the deployed BPMN condition scheme and falls back for legacy runtime instances.
openmetadata-service/src/main/java/org/openmetadata/service/migration/utils/v200/MigrationUtil.java The migration now repairs approval edges, migrated task workflow IDs, entity references, and tag suggestion payloads.

Comments Outside Diff (1)

  1. openmetadata-ui/src/main/resources/ui/src/components/WorkflowDefinitions/WorkflowBuilder/ConnectionConditionModal.tsx, line 108-133 (link)

    P1 Normalize approval casing

    An existing approval edge with condition.value set to REJECT still fails this strict option check because the available approval options are lowercase. When the modal opens, the value is treated as invalid and replaced with the default approve. Saving after that can turn the reject branch into an approve branch, or make the backend reject the workflow as missing a reject path. Normalize approval values before validating them instead of defaulting unknown casing to approve.

Reviews (20): Last reviewed commit: "Merge branch 'fix/user-approval-conditio..." | Re-trigger Greptile

Context used (3)

  • Context used - CLAUDE.md (source)
  • Context used - openmetadata-ui-core-components/CLAUDE.md (source)
  • Context used - AGENTS.md (source)

…ve/reject

Task V2 seeds use `approve`/`reject` on outbound edges from `userApprovalTask`
nodes, but the v1105 workflow migration and various runtime write paths were
still emitting the legacy `true`/`false` boolean strings. This produced
duplicate outbound edges on migrated glossary approval workflows, blocked
Task V2 tasks from resolving against pre-migration Flowable process instances,
and left auto-approval + multi-approval writing values that no longer matched
the deployed BPMN. The WorkflowBuilder UI still only offered True/False when
authoring a userApprovalTask edge, so admin-authored workflows silently ended
up with dead edges.

Backend
- Reuse a single set of condition constants (`APPROVE_CONDITION`,
  `REJECT_CONDITION`, `LEGACY_APPROVE_CONDITION`, `LEGACY_REJECT_CONDITION`)
  from `Workflow`.
- v1105 `MigrationUtil` now writes `approve`/`reject` on `ApprovalForUpdates`
  outbound edges instead of `true`/`false`.
- v200 `MigrationUtil.TaskWorkflow.runRecognizerFeedbackTaskTypeMigration` now
  runs a one-shot approval-edge condition rewrite so any DB that already ran
  the buggy v1105 migration ends up with `approve`/`reject` on
  `userApprovalTask` outbound edges and dedups duplicates left behind by
  earlier runs.
- `TaskWorkflowHandler.resolveWorkflowResult` inspects the deployed BPMN first
  via `WorkflowHandler.getExpectedResultForActiveTask`, so already-running
  Flowable process instances (frozen with `_result == 'true'`) keep firing
  their outbound edges even after the WD entity is migrated.
- `WorkflowHandler.handleMultiApproval` and
  `AutoApproveServiceTaskImpl.resolvePositiveResult` now write whichever
  condition string the deployed BPMN's outbound flow expects.
- Legacy `RecognizerFeedbackApproval`/`DataQualityReview` carveout removed
  from `TaskWorkflowHandler.resolveWorkflowResult`; the new BPMN-inspection
  fallback covers both.
- `WorkflowDefinitionRepository.validateApprovalConditions` accepts
  approve/reject (plus legacy true/false) on `userApprovalTask` outbound
  edges. Non-approval conditional nodes continue to require true/false.
- `RecognizerFeedbackReviewWorkflow.json` seed uses `approve`/`reject`.
- Restore `CreateApprovalTaskImpl` as a `@Deprecated(forRemoval = true)`
  subclass of `CreateTask` so pre-Task-V2 Flowable ProcessDefinitions whose
  frozen BPMN references the historical FQN resolve to a live class instead
  of raising `ClassNotFoundException`.

UI
- Add `ConditionValue.APPROVE`/`.REJECT` and
  `AVAILABLE_OPTIONS.APPROVAL_CONDITION_VALUES`.
- `ConnectionConditionModal` offers Approve/Reject when the source node is a
  `userApprovalTask`; other conditional nodes keep True/False.
- `WorkflowSerializer` renders `approve` in green and `reject` in red.

Tests
- Update `WorkflowDefinitionResourceIT` fixtures (29 approval outbound edges
  swapped to approve/reject) and the corresponding validation assertion.
- Update `v1105/MigrationUtilTest` fixtures + assertions for the new scheme.
- Update `WorkflowOssRestrictions.spec.ts` fixtures.
@yan-3005
yan-3005 requested a review from a team as a code owner July 10, 2026 10:53
Copilot AI review requested due to automatic review settings July 10, 2026 10:53
@yan-3005 yan-3005 added the safe to test Add this label to run secure Github workflows on PRs label Jul 10, 2026

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.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@github-actions

github-actions Bot commented Jul 10, 2026

Copy link
Copy Markdown
Contributor

✅ PR checks passed

The linked issue has a description and all required Shipping project fields set. Thanks!

Comment on lines +25 to +26
APPROVE = 'APPROVE',
REJECT = 'REJECT',

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.

P1 Uppercase Approval Conditions Break Saves

When the workflow builder sends the new approval option values, it can send APPROVE and REJECT, but the backend validation and BPMN result matching added here expect lowercase approve and reject. Editing an approval edge through the UI can therefore fail validation as missing approve/reject flows, even though the user selected the correct labels.

Suggested change
APPROVE = 'APPROVE',
REJECT = 'REJECT',
APPROVE = 'approve',
REJECT = 'reject',

Address gitar-bot review notes on #29931:
- v200 approval-edge migration: comment why createOrUpdate → deploy is
  intentional (fresh ACT_RE_PROCDEF with approve/reject conditions).
- WorkflowHandler condition regex: comment the Edge#getFlowableCondition
  contract that emits `${var == 'X'}` for every edge, including legacy
  boolean conditions, so no unquoted form ever reaches the deployed BPMN.
Copilot AI review requested due to automatic review settings July 10, 2026 11:06

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.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@github-actions

github-actions Bot commented Jul 10, 2026

Copy link
Copy Markdown
Contributor

Jest test Coverage

UI tests summary

Lines Statements Branches Functions
Coverage: 65%
65.16% (75800/116326) 48.94% (45159/92272) 49.77% (13671/27466)

Copilot AI review requested due to automatic review settings July 10, 2026 12:26

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.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@github-actions

github-actions Bot commented Jul 10, 2026

Copy link
Copy Markdown
Contributor

🔴 Playwright Results — 1 failure(s), 37 flaky

✅ 4529 passed · ❌ 1 failed · 🟡 37 flaky · ⏭️ 99 skipped

Shard Passed Failed Flaky Skipped
🟡 Shard 1 435 0 5 16
✅ Shard 2 11 0 0 0
🟡 Shard 3 816 0 16 8
🟡 Shard 4 820 0 3 18
🔴 Shard 5 834 1 2 9
🟡 Shard 6 785 0 3 46
🟡 Shard 7 828 0 8 2

Genuine Failures (failed on all attempts)

Pages/CustomProperties.spec.ts › Set & Update all CP types on chart (shard 5)
Error: �[2mexpect(�[22m�[31mlocator�[39m�[2m).�[22mtoContainText�[2m(�[22m�[32mexpected�[39m�[2m)�[22m failed

Locator: getByTestId('custom-property-cp-time-cp-531c771b.!@#%`()_-=+{}[]|;\',.?-card').getByTestId('property-value')
Expected substring: �[32m"15:35:59"�[39m
Received string:    �[31m"Not set"�[39m
Timeout: 15000ms

Call log:
�[2m  - Expect "toContainText" with timeout 15000ms�[22m
�[2m  - waiting for getByTestId('custom-property-cp-time-cp-531c771b.!@#%`()_-=+{}[]|;\',.?-card').getByTestId('property-value')�[22m
�[2m    18 × locator resolved to <div class="value-container" data-testid="property-value">…</div>�[22m
�[2m       - unexpected value "Not set"�[22m

🟡 37 flaky test(s) (passed on retry)
  • Features/Glossary/GlossaryPagination.spec.ts › should check for nested glossary term search (shard 1, 1 retry)
  • Flow/TestConnectionModal.spec.ts › modal opens with gate card and capability checks sections (shard 1, 1 retry)
  • Flow/TestConnectionModal.spec.ts › success state shows Done button and hides Edit Connection and Retry Test (shard 1, 1 retry)
  • Pages/Lineage/LineageRightPanel.spec.ts › Verify custom properties tab IS visible for supported type: metric (shard 1, 1 retry)
  • Flow/SearchRBAC.spec.ts › a fully denied user sees neither asset type when browsing (shard 1, 1 retry)
  • Features/BulkEditEntity.spec.ts › Database (shard 3, 1 retry)
  • Features/BulkEditEntity.spec.ts › Database Schema (shard 3, 1 retry)
  • Features/BulkEditEntity.spec.ts › Glossary Term (Nested) (shard 3, 1 retry)
  • Features/BulkEditOperationBadges.spec.ts › Glossary bulk edit shows UPDATE badge and increments summary after editing a cell (shard 3, 1 retry)
  • Features/BulkEditOperationBadges.spec.ts › Glossary bulk edit search filters rows and clear restores them (shard 3, 1 retry)
  • Features/BulkEditOperationBadges.spec.ts › Database service bulk edit search filters rows and clear restores them (shard 3, 1 retry)
  • Features/BulkImportWithDotInName.spec.ts › Column with dot in name under service with dot (shard 3, 1 retry)
  • Features/BulkImportWithDotInName.spec.ts › Import at database level with dot in service name (shard 3, 1 retry)
  • Features/ContextCenterArchive.spec.ts › archive page lazy-loads more rows on scroll within its own scroll container (shard 3, 1 retry)
  • Features/ContextCenterArticles.spec.ts › Article listing search filters, clears, and shows empty state (shard 3, 1 retry)
  • Features/ContextCenterArticles.spec.ts › Article list cards, recently viewed widget, and pagination work (shard 3, 1 retry)
  • Features/ContextCenterArticles.spec.ts › description: switching articles does not bleed unsaved content into next article (shard 3, 1 retry)
  • Features/ContextCenterMemories.spec.ts › editing visibility from Shared to Private saves and updates the badge (shard 3, 1 retry)
  • Features/ContextCenterPermission.spec.ts › a document archived by a different user can be permanently deleted, via the UI, by a user with Delete permission (shard 3, 1 retry)
  • Features/DataQuality/BundleSuiteBulkOperations.spec.ts › Add test case to existing Bundle Suite (shard 3, 1 retry)
  • Features/DomainTierCertificationVoting.spec.ts › DataProduct - Tier assign, update, and remove (shard 3, 1 retry)
  • Features/Glossary/GlossaryTermRelationsGraphNested.spec.ts › viewing a child term: parentOf edge is rendered between parent and child (shard 4, 1 retry)
  • Features/Glossary/LargeGlossaryPerformance.spec.ts › should expand and collapse all terms (shard 4, 1 retry)
  • Features/RestoreEntityInheritedFields.spec.ts › Validate restore with Inherited domain and data products assigned (shard 4, 1 retry)
  • Flow/IngestionBot.spec.ts › Ingestion bot should be able to access domain specific domain (shard 5, 1 retry)
  • Pages/CustomProperties.spec.ts › Date Time (shard 5, 1 retry)
  • Pages/Entity.spec.ts › Domain Propagation (shard 6, 1 retry)
  • Pages/ExploreBrowse.spec.ts › service type drill-down disables unrelated roots and query-panel Clear resets it (shard 6, 1 retry)
  • Pages/ExplorePageRightPanel_KnowledgeCenter.spec.ts › Should remove user owner for knowledgeCenter (shard 6, 1 retry)
  • Pages/ExplorePageRightPanel.spec.ts › Should allow Data Steward to edit owners for databaseSchema (shard 7, 1 retry)
  • ... and 7 more

📦 Download artifacts

How to debug locally
# Download playwright-test-results-<shard> artifact and unzip
npx playwright show-trace path/to/trace.zip    # view trace

Copilot AI review requested due to automatic review settings July 10, 2026 12:29

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.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

Comment on lines +25 to +26
APPROVE = 'APPROVE',
REJECT = 'REJECT',

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.

P1 Lowercase approval values

The approval option values are still APPROVE and REJECT. ConnectionConditionModal now uses these values for userApprovalTask edges, so a newly selected approval edge can submit uppercase conditions. The backend approval validation is case-sensitive and accepts lowercase approve/reject plus legacy true/false, so saving a workflow edited through this modal can still fail as missing approve/reject flows. Keep the option labels as Approve/Reject, but send the backend condition strings as lowercase values.

Suggested change
APPROVE = 'APPROVE',
REJECT = 'REJECT',
APPROVE = 'approve',
REJECT = 'reject',

Address greptile P1: WorkflowValidationService.toLowerCase() runs on
save, but keeping the enum literal at 'approve'/'reject' eliminates the
mismatch class entirely and matches the backend seed conditions.
Copilot AI review requested due to automatic review settings July 10, 2026 12:33
MySQL 8.0 has no `CREATE INDEX IF NOT EXISTS` (Postgres does). The
previous form failed at v200 with a syntax error and aborted the
migration on the first run. Guard the index creation via
information_schema.statistics, mirroring the approvedById guard earlier
in the same file. Postgres continues to use `CREATE INDEX IF NOT EXISTS`.

Co-Authored-By: Claude Opus 4.7 (1M context) <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.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

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.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

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.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

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.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

sonika-shah
sonika-shah previously approved these changes Jul 19, 2026

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.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

yan-3005 added 4 commits July 20, 2026 16:23
…dition-scheme

# Conflicts:
#	bootstrap/sql/migrations/native/2.0.0/mysql/schemaChanges.sql
#	bootstrap/sql/migrations/native/2.0.0/postgres/schemaChanges.sql
…nition migration

Every other version-migration that touches workflow definitions (v1105, v170,
v171, v190, v11212, v1131, v200) calls `initializeWorkflowHandler()` at the top
of `runDataMigration()`. v1122 was the only miss — it wrapped
`MigrationUtil.migrateWorkflowDefinitions()` in a defensive try/catch instead.

`migrateWorkflowDefinitions` calls `repository.createOrUpdate` on each workflow
def, which triggers `WorkflowDefinitionRepository.postUpdate` →
`WorkflowHandler.getInstance().deploy(...)`. Without initialization,
`getInstance()` throws `UnhandledServerException("WorkflowHandler is not
initialized.")` — the outer try/catch swallows it and the migration reports
SUCCESS with zero definitions actually redeployed. Workflow-backed features
(Knowledge Page approvals, Data Access Requests, Recognizer feedback,
data-quality incidents, custom workflows) may then run on stale definitions
until a server restart re-initializes and redeploys them.

Fix on both mysql/v1122 and postgres/v1122: add `initializeWorkflowHandler()`
before the migrateWorkflowDefinitions call. Keep the try/catch as
belt-and-suspenders so a single failing definition still doesn't abort the
whole hop.
Resolving a TagUpdate task on 2.0 applies the tag to the entity but leaves the
entity version, changeDescription, and taskResolved change event stale —
regressed vs 1.13 where a task-driven tag write bumped the entity minor
version. Root cause: `TaskWorkflowHandler.applyMergeTagsAction` wrote directly
to `tag_usage` via `EntityRepository.applyTags`/`applyTagsDelete`, which
sidesteps the versioned entity update path (no PATCH, no store-update, no
change event).

Route entity-level tag tasks (fieldPath null / "tags" / "entity") through
`repository.patch` via a new `FieldPathUtils.updateFieldTags` helper —
mirroring the description task's `updateFieldDescription` path. It rebuilds
the entity's `tags` list from the payload's `tagsToAdd`/`tagsToRemove`, diffs
JSON to produce a `JsonPatch`, and applies it. Version bumps once, change
event fires, alerts trigger — parity with a user-driven PATCH of entity.tags.

Column/field-level tag tasks stay on the direct DAO write: reflection-based
in-memory mutation on nested Column objects produces a JSON patch that
`storeEntity` flags as `entityChanged=false` (nested tag lists round-trip
differently through Jackson), silently dropping the tag update. Keeping the
DAO path there preserves existing behavior — tags apply but no version bump,
a pre-existing gap that is not this fix's scope.

Refactored `applyMergeTagsAction` into a 15-line orchestrator plus a
`TagResolution` record and helpers for payload extraction, patch write, and
DAO write.

Verified:
- 2.0 fresh entity-level TagUpdate resolve: tags applied + version 0.1→0.2
- 2.0 fresh column-level TagUpdate resolve: tags applied (existing behavior)
- 2.0 fresh TagUpdate close: no write, no version bump
- 1.13 pending RequestTag → migrated to 2.0 → resolve: tags applied + bumped

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.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

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.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

yan-3005 added 2 commits July 20, 2026 17:53
…prawl

The prior tag-bump fix added ~260 lines to FieldPathUtils (updateFieldTags +
container-navigation + reflection setter + nested descent helpers) but only
the entity-level path was actually wired — column/field-level was intentionally
skipped because reflection-based mutation on nested Column objects makes
storeEntity flag entityChanged=false and drop the tag update. Most of that
helper was unreachable dead weight.

Collapse the fix into TaskWorkflowHandler itself: entity-level tag tasks now
snapshot entity JSON, mutate entity.tags in-memory (union-add / remove /
dedupe by tagFQN), diff, and apply via repository.patch — the same code path
a user-driven PATCH /v1/tables/{id} on tags takes. Column-level stays on the
existing applyTags/applyTagsDelete DAO write (pre-existing behavior, no bump).

FieldPathUtils reverts to its pre-fix state.

Verified on 2.0:
- fresh entity-level TagUpdate resolve: tags applied + version 0.1→0.2
- fresh column-level TagUpdate resolve: tags applied (existing behavior)
- fresh TagUpdate close: no write, no bump

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.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@sonarqubecloud

Copy link
Copy Markdown

@sonarqubecloud

Copy link
Copy Markdown

@gitar-bot

gitar-bot Bot commented Jul 20, 2026

Copy link
Copy Markdown
Code Review 👍 Approved with suggestions 5 resolved / 6 findings

Standardizes workflow approval edges to 'approve'/'reject' across backend, UI, and migrations with runtime BPMN inspection to maintain legacy support. Address the unbounded semaphore acquire in the task-resolution process to prevent potential thread exhaustion.

💡 Performance: Task-resolution semaphore uses unbounded acquire() on request threads

📄 openmetadata-service/src/main/java/org/openmetadata/service/governance/workflows/WorkflowHandler.java:864-871

resolveTaskInternal now calls taskResolutionPermits.acquire() with no timeout, and it is reached from HTTP request threads (e.g. EntityRepository/TagRepository approval updates via TaskWorkflowHandler.resolveTask). If the 8 permits are held by resolutions that are slow or stuck touching Flowable, incoming request threads block here indefinitely, past their own client timeouts, with no upper bound on the in-JVM queue depth. Consider tryAcquire(permit, timeout) and returning false (or surfacing a 503/retryable error) when the wait exceeds a bound, so a saturated Flowable pool degrades to a fast, visible failure instead of silently piling up blocked request threads.

Bound the wait for a permit so request threads fail fast instead of blocking indefinitely.
try {
  if (!taskResolutionPermits.tryAcquire(
      RESOLUTION_PERMIT_TIMEOUT_SECONDS, TimeUnit.SECONDS)) {
    LOG.error(
        "[WorkflowTask] Timed out awaiting resolution permit for '{}'", customTaskId);
    return false;
  }
} catch (InterruptedException ie) {
  Thread.currentThread().interrupt();
  LOG.error(
      "[WorkflowTask] Interrupted while awaiting resolution permit for '{}'", customTaskId);
  return false;
}
✅ 5 resolved
Performance: v200 migration redeploys every WorkflowDefinition on edge rewrite

📄 openmetadata-service/src/main/java/org/openmetadata/service/migration/utils/v200/MigrationUtil.java:1590-1604 📄 openmetadata-service/src/main/java/org/openmetadata/service/jdbi3/WorkflowDefinitionRepository.java:47-56
migrateUserApprovalTaskEdgeConditions() calls workflowDefinitionRepository.createOrUpdate() for every WorkflowDefinition whose approval edges were rewritten. postUpdate unconditionally invokes WorkflowHandler.getInstance().deploy(...), so each migrated definition triggers a fresh Flowable BPMN deployment during startup migration. Idempotency guards (rewritten.equals(edges)) rely on EdgeDefinition having field-based equals/hashCode (generated by jsonschema2pojo, so this holds), which limits this to definitions that actually carry legacy true/false conditions — good. Impact is bounded but on installs with many approval workflows this adds a redeploy per definition to the migration step. Consider whether a direct JSON-column update (as done elsewhere in this class) would avoid the redeploy churn, or confirm the redeploy is intentionally desired here.

Quality: Condition-value extraction from BPMN relies on brittle regex

📄 openmetadata-service/src/main/java/org/openmetadata/service/governance/workflows/WorkflowHandler.java:1498 📄 openmetadata-service/src/main/java/org/openmetadata/service/governance/workflows/WorkflowHandler.java:1522-1529
getExpectedResultForActiveTask/getExpectedResultForSubprocess parse the deployed BPMN's outbound conditions with CONDITION_VALUE_PATTERN = "==\s*'([^']+)'". This only matches single-quoted equality expressions such as ${result == 'approve'}. If a legacy deployment froze an unquoted/boolean expression (e.g. ${result == true}) or used a different operator/quoting, no value is captured and the method returns null. The callers all fall back safely (multi-approval to Boolean.TRUE/FALSE, TaskWorkflowHandler to defaultWorkflowResult, AutoApprove to APPROVE_CONDITION), so this is not a crash — but on a mismatched legacy BPMN the fallback could write the wrong scheme and re-introduce the stuck-resolve the PR is fixing. Worth confirming the exact condition-expression format emitted for legacy true/false deployments so the regex is guaranteed to match, or add a boolean-literal branch.

Bug: SUPERSEDED_BY_NEWER_RUN not whitelisted in WorkflowFailureListener

📄 openmetadata-service/src/main/java/org/openmetadata/service/governance/workflows/Workflow.java:30-33
The new Workflow.SUPERSEDED_BY_NEWER_RUN constant carries a doc comment claiming it is "Whitelisted in WorkflowFailureListener so PROCESS_CANCELLED stays silent for supersede." However, WorkflowFailureListener's PROCESS_CANCELLED handler only silences two causes: "Cleanup before redeployment" and "Terminated due to conflicting workflow instance". It does NOT check for the superseded string.

When CreateTask supersedes a prior approval run, terminateWorkflowInstance -> deleteMainProcessInstanceQuietly calls runtimeService.deleteProcessInstance(mainInstance.getId(), SUPERSEDED_BY_NEWER_RUN), which fires PROCESS_CANCELLED. Because that cause is not whitelisted, the listener falls through to LOG.error("[WorkflowFailure] PROCESS_CANCELLED: {}", event) and storeFailureInDatabase(...), producing a spurious error log and a persisted failure record for what is a normal supersede path.

Add the constant to the whitelist so the documented invariant holds (and reference the shared constant rather than duplicating the string literal in both CreateTask.java and Workflow.java).

Performance: List endpoints now eagerly hydrate all heavy fields by default

📄 openmetadata-service/src/main/java/org/openmetadata/service/resources/tasks/TaskResource.java:180 📄 openmetadata-service/src/main/java/org/openmetadata/service/resources/tasks/TaskResource.java:373 📄 openmetadata-service/src/main/java/org/openmetadata/service/resources/tasks/TaskResource.java:543 📄 openmetadata-service/src/main/java/org/openmetadata/service/resources/tasks/TaskResource.java:603 📄 openmetadata-service/src/main/java/org/openmetadata/service/resources/tasks/TaskResource.java:662 📄 openmetadata-service/src/main/java/org/openmetadata/service/resources/tasks/TaskResource.java:731
Adding @DefaultValue(FIELDS) to the fields param on the six Task list endpoints (list, listDataAccessRequests, listMyAssignedTasks, listMyVisibleTasks, listMyOwnedTasks, listMyCreatedTasks) changes the default so every listed task now hydrates assignees, reviewers, watchers, about, domains, comments, createdBy and payload. Previously a null fields returned a lightweight task; now each task in a page triggers the relational/field-resolution lookups (potential per-task N+1), and with limit configurable up to 1,000,000 this can substantially increase list latency and payload size for clients that don't request fields. If the goal was only to help the UI, consider a lighter default set (e.g. omit comments/payload) or keep the default null and have callers request fields explicitly.

Quality: Duplicated legacy umbrella instance-id resolution logic

📄 openmetadata-service/src/main/java/org/openmetadata/service/migration/utils/v200/MigrationUtil.java:1485-1499 📄 openmetadata-service/src/main/java/org/openmetadata/service/migration/utils/v200/MigrationUtil.java:2248-2262
MigrationUtil.lookupLegacyUmbrellaWorkflowInstanceId(Handle, String) and TaskWorkflow.resolveLegacyUmbrellaWorkflowInstanceId(UUID) implement the same two-step ACT_HI_VARINST -> workflow_instance_state_time_series join with near-identical SQL, differing only in whether they use the raw handle or collectionDAO for the second query. Consolidate into a single helper to avoid divergence if the join logic ever changes.

🤖 Prompt for agents
Code Review: Standardizes workflow approval edges to 'approve'/'reject' across backend, UI, and migrations with runtime BPMN inspection to maintain legacy support. Address the unbounded semaphore acquire in the task-resolution process to prevent potential thread exhaustion.

1. 💡 Performance: Task-resolution semaphore uses unbounded acquire() on request threads
   Files: openmetadata-service/src/main/java/org/openmetadata/service/governance/workflows/WorkflowHandler.java:864-871

   resolveTaskInternal now calls taskResolutionPermits.acquire() with no timeout, and it is reached from HTTP request threads (e.g. EntityRepository/TagRepository approval updates via TaskWorkflowHandler.resolveTask). If the 8 permits are held by resolutions that are slow or stuck touching Flowable, incoming request threads block here indefinitely, past their own client timeouts, with no upper bound on the in-JVM queue depth. Consider tryAcquire(permit, timeout) and returning false (or surfacing a 503/retryable error) when the wait exceeds a bound, so a saturated Flowable pool degrades to a fast, visible failure instead of silently piling up blocked request threads.

   Fix (Bound the wait for a permit so request threads fail fast instead of blocking indefinitely.):
   try {
     if (!taskResolutionPermits.tryAcquire(
         RESOLUTION_PERMIT_TIMEOUT_SECONDS, TimeUnit.SECONDS)) {
       LOG.error(
           "[WorkflowTask] Timed out awaiting resolution permit for '{}'", customTaskId);
       return false;
     }
   } catch (InterruptedException ie) {
     Thread.currentThread().interrupt();
     LOG.error(
         "[WorkflowTask] Interrupted while awaiting resolution permit for '{}'", customTaskId);
     return false;
   }

Options

Display: compact → Showing less information.

Comment with these commands to change the behavior for this request:

Compact
gitar display:verbose         

Was this helpful? React with 👍 / 👎 | Gitar

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

safe to test Add this label to run secure Github workflows on PRs

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Task V2 open-task + suggestion migration: broken about.id, wid, tag apply, empty assignees on default read

5 participants