From 6db4d220989943da79934ff6572539b524d44815 Mon Sep 17 00:00:00 2001 From: radu-mocanu Date: Sun, 28 Jun 2026 00:29:34 +0300 Subject: [PATCH 1/3] feat: add timer resume time field --- packages/uipath-core/pyproject.toml | 2 +- packages/uipath-core/src/uipath/core/triggers/trigger.py | 2 ++ packages/uipath-core/uv.lock | 2 +- packages/uipath-platform/uv.lock | 2 +- packages/uipath/uv.lock | 2 +- 5 files changed, 6 insertions(+), 4 deletions(-) diff --git a/packages/uipath-core/pyproject.toml b/packages/uipath-core/pyproject.toml index 93be7ebc0..95d88c18e 100644 --- a/packages/uipath-core/pyproject.toml +++ b/packages/uipath-core/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "uipath-core" -version = "0.5.23" +version = "0.5.24" description = "UiPath Core abstractions" readme = { file = "README.md", content-type = "text/markdown" } requires-python = ">=3.11" diff --git a/packages/uipath-core/src/uipath/core/triggers/trigger.py b/packages/uipath-core/src/uipath/core/triggers/trigger.py index c897acd28..52a7da0ba 100644 --- a/packages/uipath-core/src/uipath/core/triggers/trigger.py +++ b/packages/uipath-core/src/uipath/core/triggers/trigger.py @@ -1,5 +1,6 @@ """Module defining resume trigger types and data models.""" +from datetime import datetime from enum import Enum from typing import Any @@ -87,6 +88,7 @@ class UiPathResumeTrigger(BaseModel): integration_resume: UiPathIntegrationTrigger | None = Field( default=None, alias="integrationResume" ) + resume_time: datetime | None = Field(default=None, alias="resumeTime") folder_path: str | None = Field(default=None, alias="folderPath") folder_key: str | None = Field(default=None, alias="folderKey") payload: Any | None = Field(default=None, alias="interruptObject", exclude=True) diff --git a/packages/uipath-core/uv.lock b/packages/uipath-core/uv.lock index 67d937f3a..642a4bafb 100644 --- a/packages/uipath-core/uv.lock +++ b/packages/uipath-core/uv.lock @@ -1011,7 +1011,7 @@ wheels = [ [[package]] name = "uipath-core" -version = "0.5.23" +version = "0.5.24" source = { editable = "." } dependencies = [ { name = "opentelemetry-instrumentation" }, diff --git a/packages/uipath-platform/uv.lock b/packages/uipath-platform/uv.lock index c0552fa41..78a9c7046 100644 --- a/packages/uipath-platform/uv.lock +++ b/packages/uipath-platform/uv.lock @@ -1063,7 +1063,7 @@ wheels = [ [[package]] name = "uipath-core" -version = "0.5.23" +version = "0.5.24" source = { editable = "../uipath-core" } dependencies = [ { name = "opentelemetry-instrumentation" }, diff --git a/packages/uipath/uv.lock b/packages/uipath/uv.lock index f99dd06a0..80132ce10 100644 --- a/packages/uipath/uv.lock +++ b/packages/uipath/uv.lock @@ -2659,7 +2659,7 @@ dev = [ [[package]] name = "uipath-core" -version = "0.5.23" +version = "0.5.24" source = { editable = "../uipath-core" } dependencies = [ { name = "opentelemetry-instrumentation" }, From bb72c62484b61ed160a6664f04cae2313bba7df6 Mon Sep 17 00:00:00 2001 From: radu-mocanu Date: Sun, 28 Jun 2026 00:35:56 +0300 Subject: [PATCH 2/3] feat: add wait until interrupt model --- packages/uipath-platform/pyproject.toml | 4 +- .../src/uipath/platform/common/__init__.py | 2 + .../platform/common/interrupt_models.py | 19 +++++++- .../platform/resume_triggers/_protocol.py | 25 ++++++++++ .../tests/services/test_hitl.py | 46 +++++++++++++++++++ packages/uipath-platform/uv.lock | 2 +- packages/uipath/pyproject.toml | 6 +-- packages/uipath/uv.lock | 4 +- 8 files changed, 99 insertions(+), 9 deletions(-) diff --git a/packages/uipath-platform/pyproject.toml b/packages/uipath-platform/pyproject.toml index 21c693e7b..88652427b 100644 --- a/packages/uipath-platform/pyproject.toml +++ b/packages/uipath-platform/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "uipath-platform" -version = "0.1.78" +version = "0.1.79" description = "HTTP client library for programmatic access to UiPath Platform" readme = { file = "README.md", content-type = "text/markdown" } requires-python = ">=3.11" @@ -8,7 +8,7 @@ dependencies = [ "httpx>=0.28.1", "tenacity>=9.0.0", "truststore>=0.10.1", - "uipath-core>=0.5.21, <0.6.0", + "uipath-core>=0.5.24, <0.6.0", "pydantic-function-models>=0.1.11", "sqlparse>=0.5.5", ] diff --git a/packages/uipath-platform/src/uipath/platform/common/__init__.py b/packages/uipath-platform/src/uipath/platform/common/__init__.py index 2407263ee..52d26a7eb 100644 --- a/packages/uipath-platform/src/uipath/platform/common/__init__.py +++ b/packages/uipath-platform/src/uipath/platform/common/__init__.py @@ -53,6 +53,7 @@ WaitJobRaw, WaitSystemAgent, WaitTask, + WaitUntil, ) from .paging import PagedResult @@ -92,6 +93,7 @@ "DocumentExtractionValidation", "WaitDocumentExtractionValidation", "WaitIntegrationEvent", + "WaitUntil", "RequestSpec", "Endpoint", "UiPathUrl", diff --git a/packages/uipath-platform/src/uipath/platform/common/interrupt_models.py b/packages/uipath-platform/src/uipath/platform/common/interrupt_models.py index 3b2468551..ee9104c54 100644 --- a/packages/uipath-platform/src/uipath/platform/common/interrupt_models.py +++ b/packages/uipath-platform/src/uipath/platform/common/interrupt_models.py @@ -1,8 +1,9 @@ """Models for interrupt operations in UiPath platform.""" +from datetime import datetime, timezone from typing import Annotated, Any -from pydantic import BaseModel, ConfigDict, Field, model_validator +from pydantic import BaseModel, ConfigDict, Field, field_validator, model_validator from uipath.platform.context_grounding.context_grounding_index import ( ContextGroundingIndex, @@ -279,3 +280,19 @@ class WaitIntegrationEvent(BaseModel): object_name: str filter_expression: str | None = None parameters: dict[str, str] | None = None + + +class WaitUntil(BaseModel): + """Model representing a wait until an absolute point in time.""" + + resume_time: datetime = Field(alias="resumeTime") + + model_config = ConfigDict(validate_by_name=True) + + @field_validator("resume_time") + @classmethod + def validate_resume_time(cls, value: datetime) -> datetime: + """Validate and normalize resume_time to a UTC instant.""" + if value.tzinfo is None or value.utcoffset() is None: + raise ValueError("resume_time must include timezone information") + return value.astimezone(timezone.utc) diff --git a/packages/uipath-platform/src/uipath/platform/resume_triggers/_protocol.py b/packages/uipath-platform/src/uipath/platform/resume_triggers/_protocol.py index b2dbae787..515780cf0 100644 --- a/packages/uipath-platform/src/uipath/platform/resume_triggers/_protocol.py +++ b/packages/uipath-platform/src/uipath/platform/resume_triggers/_protocol.py @@ -49,6 +49,7 @@ WaitJobRaw, WaitSystemAgent, WaitTask, + WaitUntil, ) from uipath.platform.connections import EventArguments from uipath.platform.context_grounding import DeepRagStatus, IndexStatus @@ -129,6 +130,9 @@ async def read_trigger(self, trigger: UiPathResumeTrigger) -> Any | None: UiPathRuntimeError: If reading fails, job failed, API connection failed, trigger type is unknown, or HITL feedback retrieval failed. """ + if trigger.trigger_type == UiPathResumeTriggerType.TIMER: + return {"resumeTime": serialize_object(trigger.resume_time)} + uipath = UiPath() match trigger.trigger_type: @@ -484,6 +488,9 @@ async def create_trigger(self, suspend_value: Any) -> UiPathResumeTrigger: case UiPathResumeTriggerType.INBOX: await self._handle_inbox_trigger(suspend_value, resume_trigger) + case UiPathResumeTriggerType.TIMER: + self._handle_time_trigger(suspend_value, resume_trigger) + case UiPathResumeTriggerType.DEEP_RAG: await self._handle_deep_rag_job_trigger( suspend_value, resume_trigger @@ -570,6 +577,8 @@ def _determine_trigger_type(self, value: Any) -> UiPathResumeTriggerType: return UiPathResumeTriggerType.IXP_VS_ESCALATION if isinstance(value, WaitIntegrationEvent): return UiPathResumeTriggerType.INBOX + if isinstance(value, WaitUntil): + return UiPathResumeTriggerType.TIMER # default to API trigger return UiPathResumeTriggerType.API @@ -606,6 +615,8 @@ def _determine_trigger_name(self, value: Any) -> UiPathResumeTriggerName: return UiPathResumeTriggerName.EXTRACTION if isinstance(value, WaitIntegrationEvent): return UiPathResumeTriggerName.INBOX + if isinstance(value, WaitUntil): + return UiPathResumeTriggerName.TIMER # default to API trigger return UiPathResumeTriggerName.API @@ -979,6 +990,20 @@ async def _handle_inbox_trigger( inbox_id=str(uuid.uuid4()), ) + def _handle_time_trigger( + self, value: WaitUntil, resume_trigger: UiPathResumeTrigger + ) -> None: + """Handle Timer-type resume triggers. + + Orchestrator expects timer resume triggers as a top-level + `resumeTime` value on the resume trigger DTO. + + Args: + value: The suspend value (WaitUntil) + resume_trigger: The resume trigger to populate + """ + resume_trigger.resume_time = value.resume_time + class UiPathResumeTriggerHandler: """Combined handler for creating and reading resume triggers. diff --git a/packages/uipath-platform/tests/services/test_hitl.py b/packages/uipath-platform/tests/services/test_hitl.py index 7395908a9..5151d7a7a 100644 --- a/packages/uipath-platform/tests/services/test_hitl.py +++ b/packages/uipath-platform/tests/services/test_hitl.py @@ -1,11 +1,13 @@ import json import uuid +from datetime import datetime, timedelta, timezone from typing import Any from unittest.mock import AsyncMock, patch import pytest from pytest_httpx import HTTPXMock from uipath.core.errors import ErrorCategory, UiPathFaultedTriggerError +from uipath.core.serialization import serialize_object from uipath.core.triggers import ( UiPathApiTrigger, UiPathIntegrationTrigger, @@ -39,6 +41,7 @@ WaitJobRaw, WaitSystemAgent, WaitTask, + WaitUntil, ) from uipath.platform.connections import Connection from uipath.platform.context_grounding import ( @@ -1214,6 +1217,21 @@ async def test_read_ixp_vs_escalation_trigger_unassigned( reader = UiPathResumeTriggerReader() await reader.read_trigger(resume_trigger) + @pytest.mark.anyio + async def test_read_timer_trigger_serializes_resume_time(self) -> None: + """Test reading a timer trigger returns JSON-safe resume time data.""" + resume_time = datetime(2026, 6, 27, 20, 14, 49, tzinfo=timezone.utc) + resume_trigger = UiPathResumeTrigger( + trigger_type=UiPathResumeTriggerType.TIMER, + trigger_name=UiPathResumeTriggerName.TIMER, + resume_time=resume_time, + ) + + reader = UiPathResumeTriggerReader() + result = await reader.read_trigger(resume_trigger) + + assert result == {"resumeTime": serialize_object(resume_time)} + class TestHitlProcessor: """Tests for the HitlProcessor class.""" @@ -2063,6 +2081,34 @@ async def test_create_resume_trigger_wait_document_extraction_validation( assert resume_trigger.trigger_type == UiPathResumeTriggerType.IXP_VS_ESCALATION assert resume_trigger.item_key == operation_id + @pytest.mark.anyio + async def test_create_resume_trigger_wait_until_normalizes_to_utc(self) -> None: + """Test creating a timer resume trigger for WaitUntil.""" + local_resume_time = datetime( + 2026, + 6, + 27, + 23, + 14, + 49, + tzinfo=timezone(timedelta(hours=3)), + ) + wait_until = WaitUntil(resume_time=local_resume_time) + + processor = UiPathResumeTriggerCreator() + resume_trigger = await processor.create_trigger(wait_until) + + assert resume_trigger.trigger_type == UiPathResumeTriggerType.TIMER + assert resume_trigger.trigger_name == UiPathResumeTriggerName.TIMER + assert resume_trigger.resume_time == datetime( + 2026, 6, 27, 20, 14, 49, tzinfo=timezone.utc + ) + + def test_wait_until_requires_timezone_aware_resume_time(self) -> None: + """Test WaitUntil rejects timezone-naive resume times.""" + with pytest.raises(ValueError, match="resume_time must include timezone"): + WaitUntil(resume_time=datetime(2026, 6, 27, 20, 14, 49)) + class TestDocumentExtractionModels: """Tests for document extraction models.""" diff --git a/packages/uipath-platform/uv.lock b/packages/uipath-platform/uv.lock index 78a9c7046..18142164c 100644 --- a/packages/uipath-platform/uv.lock +++ b/packages/uipath-platform/uv.lock @@ -1095,7 +1095,7 @@ dev = [ [[package]] name = "uipath-platform" -version = "0.1.78" +version = "0.1.79" source = { editable = "." } dependencies = [ { name = "httpx" }, diff --git a/packages/uipath/pyproject.toml b/packages/uipath/pyproject.toml index 62bb9a5e1..95566d744 100644 --- a/packages/uipath/pyproject.toml +++ b/packages/uipath/pyproject.toml @@ -1,13 +1,13 @@ [project] name = "uipath" -version = "2.11.13" +version = "2.11.14" description = "Python SDK and CLI for UiPath Platform, enabling programmatic interaction with automation services, process management, and deployment tools." readme = { file = "README.md", content-type = "text/markdown" } requires-python = ">=3.11" dependencies = [ - "uipath-core>=0.5.21, <0.6.0", + "uipath-core>=0.5.24, <0.6.0", "uipath-runtime>=0.11.4, <0.12.0", - "uipath-platform>=0.1.78, <0.2.0", + "uipath-platform>=0.1.79, <0.2.0", "click>=8.3.1", "httpx>=0.28.1", "pyjwt>=2.10.1", diff --git a/packages/uipath/uv.lock b/packages/uipath/uv.lock index 80132ce10..23c8a8d37 100644 --- a/packages/uipath/uv.lock +++ b/packages/uipath/uv.lock @@ -2552,7 +2552,7 @@ wheels = [ [[package]] name = "uipath" -version = "2.11.13" +version = "2.11.14" source = { editable = "." } dependencies = [ { name = "applicationinsights" }, @@ -2691,7 +2691,7 @@ dev = [ [[package]] name = "uipath-platform" -version = "0.1.78" +version = "0.1.79" source = { editable = "../uipath-platform" } dependencies = [ { name = "httpx" }, From df621cb4c12db7bf19a830526c8a409771e15c3e Mon Sep 17 00:00:00 2001 From: radu-mocanu Date: Sun, 28 Jun 2026 00:43:05 +0300 Subject: [PATCH 3/3] feat: support composite interrupt resume triggers --- packages/uipath-platform/pyproject.toml | 2 +- .../platform/resume_triggers/_protocol.py | 21 ++++++++++ .../tests/services/test_hitl.py | 39 +++++++++++++++++++ packages/uipath-platform/uv.lock | 2 +- packages/uipath/pyproject.toml | 4 +- packages/uipath/uv.lock | 4 +- 6 files changed, 66 insertions(+), 6 deletions(-) diff --git a/packages/uipath-platform/pyproject.toml b/packages/uipath-platform/pyproject.toml index 88652427b..9130b35d5 100644 --- a/packages/uipath-platform/pyproject.toml +++ b/packages/uipath-platform/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "uipath-platform" -version = "0.1.79" +version = "0.1.80" 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/src/uipath/platform/resume_triggers/_protocol.py b/packages/uipath-platform/src/uipath/platform/resume_triggers/_protocol.py index 515780cf0..fac3a4a65 100644 --- a/packages/uipath-platform/src/uipath/platform/resume_triggers/_protocol.py +++ b/packages/uipath-platform/src/uipath/platform/resume_triggers/_protocol.py @@ -443,6 +443,23 @@ class UiPathResumeTriggerCreator: Implements UiPathResumeTriggerCreatorProtocol. """ + async def create_triggers(self, suspend_value: Any) -> list[UiPathResumeTrigger]: + """Create resume triggers from a suspend value. + + Most values create a single trigger. A list or tuple creates sibling + triggers for the same interrupt; whichever one fires first resumes it. + """ + if isinstance(suspend_value, (list, tuple)): + if not suspend_value: + raise ValueError("At least one interrupt model is required.") + return [ + await self.create_trigger(child_suspend_value) + for child_suspend_value in suspend_value + ] + + resume_trigger = await self.create_trigger(suspend_value) + return [resume_trigger] + async def create_trigger(self, suspend_value: Any) -> UiPathResumeTrigger: """Create a resume trigger from a suspend value. @@ -1030,6 +1047,10 @@ async def create_trigger(self, suspend_value: Any) -> UiPathResumeTrigger: """ return await self._creator.create_trigger(suspend_value) + async def create_triggers(self, suspend_value: Any) -> list[UiPathResumeTrigger]: + """Create resume triggers from a suspend value.""" + return await self._creator.create_triggers(suspend_value) + async def read_trigger(self, trigger: UiPathResumeTrigger) -> Any | None: """Read a resume trigger and convert it to runtime-compatible input. diff --git a/packages/uipath-platform/tests/services/test_hitl.py b/packages/uipath-platform/tests/services/test_hitl.py index 5151d7a7a..134d75fc0 100644 --- a/packages/uipath-platform/tests/services/test_hitl.py +++ b/packages/uipath-platform/tests/services/test_hitl.py @@ -2109,6 +2109,45 @@ def test_wait_until_requires_timezone_aware_resume_time(self) -> None: with pytest.raises(ValueError, match="resume_time must include timezone"): WaitUntil(resume_time=datetime(2026, 6, 27, 20, 14, 49)) + @pytest.mark.anyio + async def test_create_resume_triggers_for_interrupt_list( + self, + ) -> None: + """Test an interrupt list creates sibling triggers for the same interrupt.""" + job_key = "test-job-key" + wait_job = WaitJob( + job=Job( + id=1234, + key=job_key, + folder_key="d0e09040-5997-44e1-93b7-4087689521b7", + ), + process_folder_path="/test/path", + ) + wait_until = WaitUntil( + resume_time=datetime(2026, 6, 27, 23, 14, 49, tzinfo=timezone.utc) + ) + + processor = UiPathResumeTriggerCreator() + triggers = await processor.create_triggers([wait_job, wait_until]) + + assert len(triggers) == 2 + job_trigger, timer_trigger = triggers + assert job_trigger.trigger_type == UiPathResumeTriggerType.JOB + assert job_trigger.item_key == job_key + assert timer_trigger.trigger_type == UiPathResumeTriggerType.TIMER + assert timer_trigger.trigger_name == UiPathResumeTriggerName.TIMER + assert timer_trigger.resume_time == datetime( + 2026, 6, 27, 23, 14, 49, tzinfo=timezone.utc + ) + + @pytest.mark.anyio + async def test_create_resume_triggers_rejects_empty_interrupt_list(self) -> None: + """Test an interrupt list must include at least one model.""" + processor = UiPathResumeTriggerCreator() + + with pytest.raises(ValueError, match="At least one interrupt model"): + await processor.create_triggers([]) + class TestDocumentExtractionModels: """Tests for document extraction models.""" diff --git a/packages/uipath-platform/uv.lock b/packages/uipath-platform/uv.lock index 18142164c..296825bc8 100644 --- a/packages/uipath-platform/uv.lock +++ b/packages/uipath-platform/uv.lock @@ -1095,7 +1095,7 @@ dev = [ [[package]] name = "uipath-platform" -version = "0.1.79" +version = "0.1.80" source = { editable = "." } dependencies = [ { name = "httpx" }, diff --git a/packages/uipath/pyproject.toml b/packages/uipath/pyproject.toml index 95566d744..45b789faf 100644 --- a/packages/uipath/pyproject.toml +++ b/packages/uipath/pyproject.toml @@ -1,13 +1,13 @@ [project] name = "uipath" -version = "2.11.14" +version = "2.12.0" description = "Python SDK and CLI for UiPath Platform, enabling programmatic interaction with automation services, process management, and deployment tools." readme = { file = "README.md", content-type = "text/markdown" } requires-python = ">=3.11" dependencies = [ "uipath-core>=0.5.24, <0.6.0", "uipath-runtime>=0.11.4, <0.12.0", - "uipath-platform>=0.1.79, <0.2.0", + "uipath-platform>=0.1.80, <0.2.0", "click>=8.3.1", "httpx>=0.28.1", "pyjwt>=2.10.1", diff --git a/packages/uipath/uv.lock b/packages/uipath/uv.lock index 23c8a8d37..9003b9ca9 100644 --- a/packages/uipath/uv.lock +++ b/packages/uipath/uv.lock @@ -2552,7 +2552,7 @@ wheels = [ [[package]] name = "uipath" -version = "2.11.14" +version = "2.12.0" source = { editable = "." } dependencies = [ { name = "applicationinsights" }, @@ -2691,7 +2691,7 @@ dev = [ [[package]] name = "uipath-platform" -version = "0.1.79" +version = "0.1.80" source = { editable = "../uipath-platform" } dependencies = [ { name = "httpx" },