Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion packages/uipath-core/pyproject.toml
Original file line number Diff line number Diff line change
@@ -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"
Expand Down
2 changes: 2 additions & 0 deletions packages/uipath-core/src/uipath/core/triggers/trigger.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
"""Module defining resume trigger types and data models."""

from datetime import datetime
from enum import Enum
from typing import Any

Expand Down Expand Up @@ -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)
Expand Down
2 changes: 1 addition & 1 deletion packages/uipath-core/uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions packages/uipath-platform/pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
[project]
name = "uipath-platform"
version = "0.1.78"
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"
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",
]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@
WaitJobRaw,
WaitSystemAgent,
WaitTask,
WaitUntil,
)
from .paging import PagedResult

Expand Down Expand Up @@ -92,6 +93,7 @@
"DocumentExtractionValidation",
"WaitDocumentExtractionValidation",
"WaitIntegrationEvent",
"WaitUntil",
"RequestSpec",
"Endpoint",
"UiPathUrl",
Expand Down
Original file line number Diff line number Diff line change
@@ -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,
Expand Down Expand Up @@ -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)
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@
WaitJobRaw,
WaitSystemAgent,
WaitTask,
WaitUntil,
)
from uipath.platform.connections import EventArguments
from uipath.platform.context_grounding import DeepRagStatus, IndexStatus
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -439,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.

Expand Down Expand Up @@ -484,6 +505,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
Expand Down Expand Up @@ -570,6 +594,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

Expand Down Expand Up @@ -606,6 +632,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

Expand Down Expand Up @@ -979,6 +1007,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.
Expand All @@ -1005,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.

Expand Down
85 changes: 85 additions & 0 deletions packages/uipath-platform/tests/services/test_hitl.py
Original file line number Diff line number Diff line change
@@ -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,
Expand Down Expand Up @@ -39,6 +41,7 @@
WaitJobRaw,
WaitSystemAgent,
WaitTask,
WaitUntil,
)
from uipath.platform.connections import Connection
from uipath.platform.context_grounding import (
Expand Down Expand Up @@ -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."""
Expand Down Expand Up @@ -2063,6 +2081,73 @@ 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))

@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."""
Expand Down
4 changes: 2 additions & 2 deletions packages/uipath-platform/uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 3 additions & 3 deletions packages/uipath/pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
[project]
name = "uipath"
version = "2.11.13"
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.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.80, <0.2.0",
"click>=8.3.1",
"httpx>=0.28.1",
"pyjwt>=2.10.1",
Expand Down
6 changes: 3 additions & 3 deletions packages/uipath/uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.