From 218747aa9ac937f659cbd1d0c3a154954c770f46 Mon Sep 17 00:00:00 2001 From: Sahil Wakode Date: Mon, 19 Oct 2026 16:04:04 +0530 Subject: [PATCH 1/6] feat(tasks): accept task_source_metadata when creating quick form tasks [ACTN-11909] --- .../platform/action_center/_tasks_service.py | 9 +++++++- .../tests/services/test_actions_service.py | 22 +++++++++++++++++++ 2 files changed, 30 insertions(+), 1 deletion(-) diff --git a/packages/uipath-platform/src/uipath/platform/action_center/_tasks_service.py b/packages/uipath-platform/src/uipath/platform/action_center/_tasks_service.py index 36e2a012e..8a956b4f0 100644 --- a/packages/uipath-platform/src/uipath/platform/action_center/_tasks_service.py +++ b/packages/uipath-platform/src/uipath/platform/action_center/_tasks_service.py @@ -274,6 +274,7 @@ def _create_quickform_spec( is_actionable_message_enabled: Optional[bool] = None, actionable_message_metadata: Optional[Dict[str, Any]] = None, source_name: str = "Agent", + task_source_metadata: Optional[Dict[str, Any]] = None, ) -> RequestSpec: """Build the RequestSpec for Orchestrator's GenericTasks/CreateTask endpoint. @@ -304,7 +305,7 @@ def _create_quickform_spec( ) if actionable_message_metadata is not None: json_payload["actionableMessageMetaData"] = actionable_message_metadata - _apply_task_source(json_payload, source_name) + _apply_task_source(json_payload, source_name, custom_metadata=task_source_metadata) return RequestSpec( method="POST", @@ -704,6 +705,7 @@ async def create_quickform_async( actionable_message_metadata: Optional[Dict[str, Any]] = None, creator_job_key: Optional[str] = None, source_name: str = "Agent", + task_source_metadata: Optional[Dict[str, Any]] = None, ) -> Task: """Creates a new QuickForm task asynchronously. @@ -737,6 +739,8 @@ async def create_quickform_async( creator_job_key: Optional. Identifies the job that triggered the inline schema creation/upsert. source_name: Source name on TaskSource. Defaults to 'Agent'. + task_source_metadata: Optional extra keys merged into + taskSource.taskSourceMetadata, same as in `create_async()`. Returns: Task: The created task object. @@ -754,6 +758,7 @@ async def create_quickform_async( is_actionable_message_enabled=is_actionable_message_enabled, actionable_message_metadata=actionable_message_metadata, source_name=source_name, + task_source_metadata=task_source_metadata, ) response = await self.request_async( @@ -794,6 +799,7 @@ def create_quickform( actionable_message_metadata: Optional[Dict[str, Any]] = None, creator_job_key: Optional[str] = None, source_name: str = "Agent", + task_source_metadata: Optional[Dict[str, Any]] = None, ) -> Task: """Create a new QuickForm task synchronously. @@ -812,6 +818,7 @@ def create_quickform( is_actionable_message_enabled=is_actionable_message_enabled, actionable_message_metadata=actionable_message_metadata, source_name=source_name, + task_source_metadata=task_source_metadata, ) response = self.request( diff --git a/packages/uipath-platform/tests/services/test_actions_service.py b/packages/uipath-platform/tests/services/test_actions_service.py index c70f2557f..6102c6fab 100644 --- a/packages/uipath-platform/tests/services/test_actions_service.py +++ b/packages/uipath-platform/tests/services/test_actions_service.py @@ -1071,6 +1071,28 @@ async def test_create_async_merges_task_source_metadata( assert task_source_metadata["InstanceId"] == "trace-1" +def test_create_quickform_merges_task_source_metadata( + qf_runner: Any, jit_debug_env: None +) -> None: + _, body = qf_runner(task_source_metadata={"ParentSpanId": "a3c27222815e80ca"}) + + task_source_metadata = body["taskSource"]["taskSourceMetadata"] + assert task_source_metadata["ParentSpanId"] == "a3c27222815e80ca" + assert task_source_metadata["InstanceId"] == "trace-1" + + +async def test_create_quickform_async_merges_task_source_metadata( + qf_runner_async: Any, jit_debug_env: None +) -> None: + _, body = await qf_runner_async( + task_source_metadata={"ParentSpanId": "a3c27222815e80ca"} + ) + + task_source_metadata = body["taskSource"]["taskSourceMetadata"] + assert task_source_metadata["ParentSpanId"] == "a3c27222815e80ca" + assert task_source_metadata["InstanceId"] == "trace-1" + + def test_create_omits_extra_task_source_metadata_when_unset( httpx_mock: HTTPXMock, service: TasksService, From a84b8b5c5b4278050e40e5ba822141057d244544 Mon Sep 17 00:00:00 2001 From: Sahil Wakode Date: Mon, 19 Oct 2026 17:37:33 +0530 Subject: [PATCH 2/6] test(tasks): share the quick form task source assertions --- .../tests/services/test_actions_service.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/packages/uipath-platform/tests/services/test_actions_service.py b/packages/uipath-platform/tests/services/test_actions_service.py index 6102c6fab..58e571c47 100644 --- a/packages/uipath-platform/tests/services/test_actions_service.py +++ b/packages/uipath-platform/tests/services/test_actions_service.py @@ -1071,14 +1071,17 @@ async def test_create_async_merges_task_source_metadata( assert task_source_metadata["InstanceId"] == "trace-1" +def _assert_quickform_task_source(body: dict[str, Any]) -> None: + task_source_metadata = body["taskSource"]["taskSourceMetadata"] + assert task_source_metadata["ParentSpanId"] == "a3c27222815e80ca" + assert task_source_metadata["InstanceId"] == "trace-1" + + def test_create_quickform_merges_task_source_metadata( qf_runner: Any, jit_debug_env: None ) -> None: _, body = qf_runner(task_source_metadata={"ParentSpanId": "a3c27222815e80ca"}) - - task_source_metadata = body["taskSource"]["taskSourceMetadata"] - assert task_source_metadata["ParentSpanId"] == "a3c27222815e80ca" - assert task_source_metadata["InstanceId"] == "trace-1" + _assert_quickform_task_source(body) async def test_create_quickform_async_merges_task_source_metadata( @@ -1087,10 +1090,7 @@ async def test_create_quickform_async_merges_task_source_metadata( _, body = await qf_runner_async( task_source_metadata={"ParentSpanId": "a3c27222815e80ca"} ) - - task_source_metadata = body["taskSource"]["taskSourceMetadata"] - assert task_source_metadata["ParentSpanId"] == "a3c27222815e80ca" - assert task_source_metadata["InstanceId"] == "trace-1" + _assert_quickform_task_source(body) def test_create_omits_extra_task_source_metadata_when_unset( From 6d6a93ab91705550009dedc44645c6cb4924d6d7 Mon Sep 17 00:00:00 2001 From: Sahil Wakode Date: Thu, 22 Oct 2026 14:52:54 +0530 Subject: [PATCH 3/6] refactor(tasks): accept task_source_metadata only on create_quickform_async --- .../platform/action_center/_tasks_service.py | 2 -- .../tests/services/test_actions_service.py | 18 ++++-------------- 2 files changed, 4 insertions(+), 16 deletions(-) diff --git a/packages/uipath-platform/src/uipath/platform/action_center/_tasks_service.py b/packages/uipath-platform/src/uipath/platform/action_center/_tasks_service.py index 8a956b4f0..7032c4249 100644 --- a/packages/uipath-platform/src/uipath/platform/action_center/_tasks_service.py +++ b/packages/uipath-platform/src/uipath/platform/action_center/_tasks_service.py @@ -799,7 +799,6 @@ def create_quickform( actionable_message_metadata: Optional[Dict[str, Any]] = None, creator_job_key: Optional[str] = None, source_name: str = "Agent", - task_source_metadata: Optional[Dict[str, Any]] = None, ) -> Task: """Create a new QuickForm task synchronously. @@ -818,7 +817,6 @@ def create_quickform( is_actionable_message_enabled=is_actionable_message_enabled, actionable_message_metadata=actionable_message_metadata, source_name=source_name, - task_source_metadata=task_source_metadata, ) response = self.request( diff --git a/packages/uipath-platform/tests/services/test_actions_service.py b/packages/uipath-platform/tests/services/test_actions_service.py index 58e571c47..63538cf99 100644 --- a/packages/uipath-platform/tests/services/test_actions_service.py +++ b/packages/uipath-platform/tests/services/test_actions_service.py @@ -1071,26 +1071,16 @@ async def test_create_async_merges_task_source_metadata( assert task_source_metadata["InstanceId"] == "trace-1" -def _assert_quickform_task_source(body: dict[str, Any]) -> None: - task_source_metadata = body["taskSource"]["taskSourceMetadata"] - assert task_source_metadata["ParentSpanId"] == "a3c27222815e80ca" - assert task_source_metadata["InstanceId"] == "trace-1" - - -def test_create_quickform_merges_task_source_metadata( - qf_runner: Any, jit_debug_env: None -) -> None: - _, body = qf_runner(task_source_metadata={"ParentSpanId": "a3c27222815e80ca"}) - _assert_quickform_task_source(body) - - async def test_create_quickform_async_merges_task_source_metadata( qf_runner_async: Any, jit_debug_env: None ) -> None: _, body = await qf_runner_async( task_source_metadata={"ParentSpanId": "a3c27222815e80ca"} ) - _assert_quickform_task_source(body) + + task_source_metadata = body["taskSource"]["taskSourceMetadata"] + assert task_source_metadata["ParentSpanId"] == "a3c27222815e80ca" + assert task_source_metadata["InstanceId"] == "trace-1" def test_create_omits_extra_task_source_metadata_when_unset( From 404dcf920e017ef6bbd18c562f50e4d82836b8f4 Mon Sep 17 00:00:00 2001 From: Sahil Wakode Date: Thu, 22 Oct 2026 15:11:03 +0530 Subject: [PATCH 4/6] chore(tasks): bump uipath-platform to 0.2.42 --- packages/uipath-platform/pyproject.toml | 2 +- packages/uipath-platform/uv.lock | 2 +- packages/uipath/uv.lock | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/uipath-platform/pyproject.toml b/packages/uipath-platform/pyproject.toml index c3e51954e..512c3a940 100644 --- a/packages/uipath-platform/pyproject.toml +++ b/packages/uipath-platform/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "uipath-platform" -version = "0.2.41" +version = "0.2.42" description = "HTTP client library for programmatic access to UiPath Platform" readme = { file = "README.md", content-type = "text/markdown" } requires-python = ">=3.11" diff --git a/packages/uipath-platform/uv.lock b/packages/uipath-platform/uv.lock index 5c59bdb8e..3ef58613d 100644 --- a/packages/uipath-platform/uv.lock +++ b/packages/uipath-platform/uv.lock @@ -1095,7 +1095,7 @@ dev = [ [[package]] name = "uipath-platform" -version = "0.2.41" +version = "0.2.42" source = { editable = "." } dependencies = [ { name = "anyio" }, diff --git a/packages/uipath/uv.lock b/packages/uipath/uv.lock index e6ae1076e..25fa35fa2 100644 --- a/packages/uipath/uv.lock +++ b/packages/uipath/uv.lock @@ -2762,7 +2762,7 @@ wheels = [ [[package]] name = "uipath-platform" -version = "0.2.41" +version = "0.2.42" source = { editable = "../uipath-platform" } dependencies = [ { name = "anyio" }, From 0ef02b59c30350b295c388b378540748ebcc3dd0 Mon Sep 17 00:00:00 2001 From: Sahil Wakode Date: Mon, 26 Oct 2026 20:49:52 +0530 Subject: [PATCH 5/6] feat(tasks): send the current span as ParentSpanId in taskSourceMetadata [ACTN-11909] _apply_task_source adds ParentSpanId from the current span, so every task the SDK creates carries it. The task_source_metadata parameter on create_quickform_async is no longer needed and is removed. --- .../platform/action_center/_tasks_service.py | 13 +++++++------ .../tests/services/test_actions_service.py | 15 ++++++++++----- 2 files changed, 17 insertions(+), 11 deletions(-) diff --git a/packages/uipath-platform/src/uipath/platform/action_center/_tasks_service.py b/packages/uipath-platform/src/uipath/platform/action_center/_tasks_service.py index 7032c4249..103e3422a 100644 --- a/packages/uipath-platform/src/uipath/platform/action_center/_tasks_service.py +++ b/packages/uipath-platform/src/uipath/platform/action_center/_tasks_service.py @@ -3,11 +3,14 @@ import uuid from typing import Any, Dict, List, Optional +from opentelemetry import trace +from opentelemetry.trace import format_span_id from uipath.core.feature_flags import ( JIT_ESCALATION_APPS_FEATURE_FLAG, FeatureFlags, ) from uipath.core.tracing import traced +from uipath.core.tracing.span_utils import UiPathSpanUtils from uipath.platform.constants import ( ENV_TENANT_ID, @@ -210,6 +213,8 @@ def _apply_task_source( trace_id = UiPathConfig.trace_id if not (project_id and trace_id): return + span = UiPathSpanUtils.get_external_current_span() or trace.get_current_span() + span_id = span.get_span_context().span_id payload["taskSource"] = { "sourceName": source_name, "sourceId": project_id, @@ -218,6 +223,7 @@ def _apply_task_source( "FolderKey": UiPathConfig.folder_key, "JobKey": UiPathConfig.job_key, "ProcessKey": UiPathConfig.process_uuid, + **({"ParentSpanId": format_span_id(span_id)} if span_id else {}), **(custom_metadata or {}), }, "jobId": UiPathConfig.job_key, @@ -274,7 +280,6 @@ def _create_quickform_spec( is_actionable_message_enabled: Optional[bool] = None, actionable_message_metadata: Optional[Dict[str, Any]] = None, source_name: str = "Agent", - task_source_metadata: Optional[Dict[str, Any]] = None, ) -> RequestSpec: """Build the RequestSpec for Orchestrator's GenericTasks/CreateTask endpoint. @@ -305,7 +310,7 @@ def _create_quickform_spec( ) if actionable_message_metadata is not None: json_payload["actionableMessageMetaData"] = actionable_message_metadata - _apply_task_source(json_payload, source_name, custom_metadata=task_source_metadata) + _apply_task_source(json_payload, source_name) return RequestSpec( method="POST", @@ -705,7 +710,6 @@ async def create_quickform_async( actionable_message_metadata: Optional[Dict[str, Any]] = None, creator_job_key: Optional[str] = None, source_name: str = "Agent", - task_source_metadata: Optional[Dict[str, Any]] = None, ) -> Task: """Creates a new QuickForm task asynchronously. @@ -739,8 +743,6 @@ async def create_quickform_async( creator_job_key: Optional. Identifies the job that triggered the inline schema creation/upsert. source_name: Source name on TaskSource. Defaults to 'Agent'. - task_source_metadata: Optional extra keys merged into - taskSource.taskSourceMetadata, same as in `create_async()`. Returns: Task: The created task object. @@ -758,7 +760,6 @@ async def create_quickform_async( is_actionable_message_enabled=is_actionable_message_enabled, actionable_message_metadata=actionable_message_metadata, source_name=source_name, - task_source_metadata=task_source_metadata, ) response = await self.request_async( diff --git a/packages/uipath-platform/tests/services/test_actions_service.py b/packages/uipath-platform/tests/services/test_actions_service.py index 63538cf99..18b2b6a92 100644 --- a/packages/uipath-platform/tests/services/test_actions_service.py +++ b/packages/uipath-platform/tests/services/test_actions_service.py @@ -2,7 +2,9 @@ from typing import Any import pytest +from opentelemetry.trace import NonRecordingSpan, SpanContext from pytest_httpx import HTTPXMock +from uipath.core.tracing.span_utils import UiPathSpanUtils from uipath.platform import UiPathApiConfig, UiPathExecutionContext from uipath.platform.action_center import Task @@ -1071,16 +1073,19 @@ async def test_create_async_merges_task_source_metadata( assert task_source_metadata["InstanceId"] == "trace-1" -async def test_create_quickform_async_merges_task_source_metadata( - qf_runner_async: Any, jit_debug_env: None +async def test_create_quickform_async_sends_the_current_span_as_parent_span_id( + qf_runner_async: Any, jit_debug_env: None, monkeypatch: pytest.MonkeyPatch ) -> None: - _, body = await qf_runner_async( - task_source_metadata={"ParentSpanId": "a3c27222815e80ca"} + span = NonRecordingSpan( + SpanContext(trace_id=1, span_id=0xA3C27222815E80CA, is_remote=False) ) + monkeypatch.setattr(UiPathSpanUtils, "get_external_current_span", lambda: span) + monkeypatch.setenv("UIPATH_TRACE_ID", "4bf92f3577b34da6a3ce929d0e0e4736") + + _, body = await qf_runner_async() task_source_metadata = body["taskSource"]["taskSourceMetadata"] assert task_source_metadata["ParentSpanId"] == "a3c27222815e80ca" - assert task_source_metadata["InstanceId"] == "trace-1" def test_create_omits_extra_task_source_metadata_when_unset( From 873c0959acaf98b03b3a56bf8df7d283491f9cfb Mon Sep 17 00:00:00 2001 From: Sahil Wakode Date: Tue, 27 Oct 2026 20:41:05 +0530 Subject: [PATCH 6/6] fix(tasks): never let the span lookup fail task creation [ACTN-11909] ParentSpanId is added only for a valid, non-zero span id. Any error while reading the span is logged at debug level and the task is created without ParentSpanId. --- .../platform/action_center/_tasks_service.py | 19 ++++++-- .../tests/services/test_actions_service.py | 47 ++++++++++++++++++- 2 files changed, 61 insertions(+), 5 deletions(-) diff --git a/packages/uipath-platform/src/uipath/platform/action_center/_tasks_service.py b/packages/uipath-platform/src/uipath/platform/action_center/_tasks_service.py index 103e3422a..b0dd9c8da 100644 --- a/packages/uipath-platform/src/uipath/platform/action_center/_tasks_service.py +++ b/packages/uipath-platform/src/uipath/platform/action_center/_tasks_service.py @@ -1,4 +1,5 @@ import asyncio +import logging import os import uuid from typing import Any, Dict, List, Optional @@ -26,6 +27,8 @@ from .task_schema import TaskSchema from .tasks import Task, TaskRecipient, TaskRecipientType +logger = logging.getLogger(__name__) + def _ensure_string_value(value: Any) -> str: """Convert any value to a string for use in field Value.""" @@ -193,6 +196,17 @@ def _apply_priority_labels_and_actionable_toggle( payload["isActionableMessageEnabled"] = is_actionable_message_enabled +def _current_span_id() -> Optional[str]: + try: + span = UiPathSpanUtils.get_external_current_span() or trace.get_current_span() + span_context = span.get_span_context() + if span_context.is_valid and span_context.span_id: + return format_span_id(span_context.span_id) + except Exception: + logger.debug("Could not read the current span id", exc_info=True) + return None + + def _apply_task_source( payload: Dict[str, Any], source_name: str, @@ -213,8 +227,7 @@ def _apply_task_source( trace_id = UiPathConfig.trace_id if not (project_id and trace_id): return - span = UiPathSpanUtils.get_external_current_span() or trace.get_current_span() - span_id = span.get_span_context().span_id + parent_span_id = _current_span_id() payload["taskSource"] = { "sourceName": source_name, "sourceId": project_id, @@ -223,7 +236,7 @@ def _apply_task_source( "FolderKey": UiPathConfig.folder_key, "JobKey": UiPathConfig.job_key, "ProcessKey": UiPathConfig.process_uuid, - **({"ParentSpanId": format_span_id(span_id)} if span_id else {}), + **({"ParentSpanId": parent_span_id} if parent_span_id else {}), **(custom_metadata or {}), }, "jobId": UiPathConfig.job_key, diff --git a/packages/uipath-platform/tests/services/test_actions_service.py b/packages/uipath-platform/tests/services/test_actions_service.py index 18b2b6a92..f4505d1bd 100644 --- a/packages/uipath-platform/tests/services/test_actions_service.py +++ b/packages/uipath-platform/tests/services/test_actions_service.py @@ -1,14 +1,18 @@ import json from typing import Any +from unittest.mock import MagicMock import pytest -from opentelemetry.trace import NonRecordingSpan, SpanContext +from opentelemetry.trace import INVALID_SPAN, NonRecordingSpan, SpanContext from pytest_httpx import HTTPXMock from uipath.core.tracing.span_utils import UiPathSpanUtils from uipath.platform import UiPathApiConfig, UiPathExecutionContext from uipath.platform.action_center import Task -from uipath.platform.action_center._tasks_service import TasksService +from uipath.platform.action_center._tasks_service import ( + TasksService, + _apply_task_source, +) from uipath.platform.action_center.tasks import TaskRecipient, TaskRecipientType from uipath.platform.constants import HEADER_USER_AGENT @@ -1088,6 +1092,45 @@ async def test_create_quickform_async_sends_the_current_span_as_parent_span_id( assert task_source_metadata["ParentSpanId"] == "a3c27222815e80ca" +class _BrokenSpan: + def get_span_context(self) -> Any: + raise RuntimeError("span broke") + + +def _span_with_id(span_id: Any) -> Any: + span = MagicMock() + span.get_span_context.return_value.span_id = span_id + span.get_span_context.return_value.is_valid = True + return span + + +@pytest.mark.parametrize( + "span", + [ + INVALID_SPAN, + _BrokenSpan(), + _span_with_id(0), + _span_with_id(None), + _span_with_id(""), + _span_with_id("abc"), + _span_with_id(1.5), + NonRecordingSpan(SpanContext(trace_id=1, span_id=-1, is_remote=False)), + NonRecordingSpan(SpanContext(trace_id=1, span_id=2**64, is_remote=False)), + ], +) +def test_apply_task_source_without_a_valid_span_omits_parent_span_id( + jit_debug_env: None, monkeypatch: pytest.MonkeyPatch, span: Any +) -> None: + monkeypatch.setattr(UiPathSpanUtils, "get_external_current_span", lambda: span) + payload: dict[str, Any] = {} + + _apply_task_source(payload, "Agent") + + task_source_metadata = payload["taskSource"]["taskSourceMetadata"] + assert task_source_metadata["InstanceId"] == "trace-1" + assert "ParentSpanId" not in task_source_metadata + + def test_create_omits_extra_task_source_metadata_when_unset( httpx_mock: HTTPXMock, service: TasksService,