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-platform/pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[project]
name = "uipath-platform"
version = "0.1.64"
version = "0.1.65"
description = "HTTP client library for programmatic access to UiPath Platform"
readme = { file = "README.md", content-type = "text/markdown" }
requires-python = ">=3.11"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -305,6 +305,34 @@ async def _assign_task_spec(
}
]
}
elif task_recipient.type == TaskRecipientType.WORKLOAD:

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.

NIT - Can clarify here that even custom assignments will follow this criteria

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Good call — added an inline comment in the WORKLOAD branch of _assign_task_spec clarifying that this branch handles both agent-Workload recipients and agent-CustomAssignees (which map to Workload server-side).

# This branch covers BOTH agent-side Workload criteria (single
# group, distributed by workload) AND agent-side CustomAssignees
# criteria (explicit email list — already resolved into
# `task_recipient.values` upstream). Both submit to the Action
# Center API as a "Workload" assignment; the difference is whether
# `values` carries one group or N emails.
request_spec.json = {
"taskAssignments": [
{
"taskId": task_key,
"assignmentCriteria": "Workload",
"assigneeNamesOrEmails": task_recipient.values
or [recipient_value],
}
]
}
elif task_recipient.type == TaskRecipientType.ROUND_ROBIN:
request_spec.json = {
"taskAssignments": [
{
"taskId": task_key,
"assignmentCriteria": "RoundRobin",
"assigneeNamesOrEmails": task_recipient.values
or [recipient_value],
}
]
}
else:
request_spec.json = {
"taskAssignments": [
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,18 +22,35 @@ class TaskRecipientType(str, enum.Enum):
GROUP_ID = "GroupId"
EMAIL = "UserEmail"
GROUP_NAME = "GroupName"
WORKLOAD = "Workload"
ROUND_ROBIN = "RoundRobin"

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.

Should CustomAssignments be required as an option here?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Intentionally omitted: TaskRecipient is the platform-API enum and the API has no CUSTOM_ASSIGNEES member — agent-side CustomAssignees resolves to Workload (type 9) before reaching the task-creation endpoint (see _assign_task_spec in _tasks_service.py). Added a docstring note on the enum to make this explicit.



class TaskRecipient(BaseModel):
"""Model representing a task recipient."""
"""Model representing a task recipient.

`value` is the single identifier (group name, group id, user id, email, …).
`values` is the multi-assignee form used by Workload-with-custom-emails
assignments; when set it takes precedence over `value` for the
`assigneeNamesOrEmails` payload.

Note: there is no CustomAssignees member here on purpose. The agent-side
CustomAssignees criteria (AgentEscalationRecipientType.CUSTOM_ASSIGNEES,
type 11) is resolved to a Workload assignment with the explicit email list
in `values` before reaching this layer, so the Action Center
AssignTasks API only ever sees the existing literal types.
"""

type: Literal[
TaskRecipientType.USER_ID,
TaskRecipientType.GROUP_ID,
TaskRecipientType.EMAIL,
TaskRecipientType.GROUP_NAME,
TaskRecipientType.WORKLOAD,
TaskRecipientType.ROUND_ROBIN,
] = Field(..., alias="type")
value: str = Field(..., alias="value")
values: Optional[List[str]] = Field(default=None, alias="values")
display_name: Optional[str] = Field(default=None, alias="displayName")


Expand Down
162 changes: 162 additions & 0 deletions packages/uipath-platform/tests/services/test_actions_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
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 import TaskRecipient, TaskRecipientType
from uipath.platform.common.constants import HEADER_USER_AGENT


Expand Down Expand Up @@ -186,6 +187,167 @@ def test_create_with_assignee(
assert action.title == "Test Action"


def _mock_app_lookup_and_create(
httpx_mock: HTTPXMock,
base_url: str,
org: str,
tenant: str,
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""Common httpx mock setup for app lookup + task creation + assign."""
monkeypatch.setenv("UIPATH_TENANT_ID", "test-tenant-id")
httpx_mock.add_response(
url=f"{base_url}{org}/apps_/default/api/v1/default/deployed-action-apps-schemas?search=test-app&filterByDeploymentTitle=true",
status_code=200,
json={
"deployed": [
{
"systemName": "test-app",
"deploymentTitle": "test-app",
"actionSchema": {
"key": "test-key",
"inputs": [],
"outputs": [],
"inOuts": [],
"outcomes": [],
},
"deploymentFolder": {
"fullyQualifiedName": "test-folder-path",
"key": "test-folder-key",
},
}
]
},
)
httpx_mock.add_response(
url=f"{base_url}{org}{tenant}/orchestrator_/tasks/AppTasks/CreateAppTask",
status_code=200,
json={"id": 1, "title": "Test Action"},
)
httpx_mock.add_response(
url=f"{base_url}{org}{tenant}/orchestrator_/odata/Tasks/UiPath.Server.Configuration.OData.AssignTasks",
status_code=200,
json={},
)


def _assign_request_payload(httpx_mock: HTTPXMock) -> dict[str, Any]:
"""Return the parsed JSON body of the last AssignTasks request captured by the mock."""
assign_request = next(
req
for req in reversed(httpx_mock.get_requests())
if "AssignTasks" in str(req.url)
)
return json.loads(assign_request.content)


class TestAssignTaskSpec:
"""Tests for the task-assignment payload built by `_assign_task_spec`."""

def test_assign_workload_recipient_uses_workload_criteria_with_group(
self,
httpx_mock: HTTPXMock,
service: TasksService,
base_url: str,
org: str,
tenant: str,
monkeypatch: pytest.MonkeyPatch,
) -> None:
_mock_app_lookup_and_create(httpx_mock, base_url, org, tenant, monkeypatch)

service.create(
title="Test Action",
app_name="test-app",
data={"x": 1},
recipient=TaskRecipient(
type=TaskRecipientType.WORKLOAD,
value="Support Team",
displayName="Support Team",
),
)

payload = _assign_request_payload(httpx_mock)
assert payload == {
"taskAssignments": [
{
"taskId": 1,
"assignmentCriteria": "Workload",
"assigneeNamesOrEmails": ["Support Team"],
}
]
}

def test_assign_round_robin_recipient_uses_round_robin_criteria(
self,
httpx_mock: HTTPXMock,
service: TasksService,
base_url: str,
org: str,
tenant: str,
monkeypatch: pytest.MonkeyPatch,
) -> None:
_mock_app_lookup_and_create(httpx_mock, base_url, org, tenant, monkeypatch)

service.create(
title="Test Action",
app_name="test-app",
data={"x": 1},
recipient=TaskRecipient(
type=TaskRecipientType.ROUND_ROBIN,
value="Support Team",
displayName="Support Team",
),
)

payload = _assign_request_payload(httpx_mock)
assert payload == {
"taskAssignments": [
{
"taskId": 1,
"assignmentCriteria": "RoundRobin",
"assigneeNamesOrEmails": ["Support Team"],
}
]
}

def test_assign_workload_with_multiple_emails_uses_values_list(
self,
httpx_mock: HTTPXMock,
service: TasksService,
base_url: str,
org: str,
tenant: str,
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""Custom-assignees path: Workload criteria with a list of emails."""
_mock_app_lookup_and_create(httpx_mock, base_url, org, tenant, monkeypatch)

service.create(
title="Test Action",
app_name="test-app",
data={"x": 1},
recipient=TaskRecipient(
type=TaskRecipientType.WORKLOAD,
value="alice@example.com",
values=["alice@example.com", "bob@example.com"],
),
)

payload = _assign_request_payload(httpx_mock)
assert payload == {
"taskAssignments": [
{
"taskId": 1,
"assignmentCriteria": "Workload",
"assigneeNamesOrEmails": [
"alice@example.com",
"bob@example.com",
],
}
]
}


def _make_deployed_app(
name: str,
folder_path: str,
Expand Down
2 changes: 1 addition & 1 deletion packages/uipath-platform/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/pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
[project]
name = "uipath"
version = "2.10.82"
version = "2.10.83"
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.17, <0.6.0",
"uipath-runtime>=0.11.0, <0.12.0",
"uipath-platform>=0.1.63, <0.2.0",
"uipath-platform>=0.1.65, <0.2.0",
"click>=8.3.1",
"httpx>=0.28.1",
"pyjwt>=2.10.1",
Expand Down
Loading
Loading