Skip to content

Commit 42d2acc

Browse files
committed
feat: add Property binding resource support to SDK
Adds support for the Property resource type in bindings.json, enabling users to read connector-defined property values (e.g. SharePoint folder IDs) without writing custom helpers. - Extend BindingResourceValue with optional description/propertyName fields - Add PropertyResourceOverwrite to the resource overwrite discriminated union - Add BindingsService with get_property() that reads from bindings.json and respects runtime resource overwrites via the existing ContextVar - Expose sdk.bindings as a cached_property on the UiPath class - Update bindings.spec.md to document Property as the 7th resource type - Add unit tests covering file reads, suffix key matching, runtime overwrites, and error cases
1 parent 20d0b75 commit 42d2acc

7 files changed

Lines changed: 457 additions & 7 deletions

File tree

packages/uipath-platform/src/uipath/platform/_uipath.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
from .chat import ConversationsService, UiPathLlmChatService, UiPathOpenAIService
1212
from .common import (
1313
ApiClient,
14+
BindingsService,
1415
ExternalApplicationService,
1516
UiPathApiConfig,
1617
UiPathExecutionContext,
@@ -76,6 +77,10 @@ def __init__(
7677
raise SecretMissingError() from e
7778
self._execution_context = UiPathExecutionContext()
7879

80+
@cached_property
81+
def bindings(self) -> BindingsService:
82+
return BindingsService()
83+
7984
@property
8085
def api_client(self) -> ApiClient:
8186
return ApiClient(self._config, self._execution_context)

packages/uipath-platform/src/uipath/platform/common/__init__.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,13 @@
88
from ._bindings import (
99
ConnectionResourceOverwrite,
1010
GenericResourceOverwrite,
11+
PropertyResourceOverwrite,
1112
ResourceOverwrite,
1213
ResourceOverwriteParser,
1314
ResourceOverwritesContext,
1415
resource_override,
1516
)
17+
from ._bindings_service import BindingsService
1618
from ._config import UiPathApiConfig, UiPathConfig
1719
from ._endpoints_manager import EndpointManager
1820
from ._execution_context import UiPathExecutionContext
@@ -99,8 +101,10 @@
99101
"validate_pagination_params",
100102
"EndpointManager",
101103
"jsonschema_to_pydantic",
104+
"BindingsService",
102105
"ConnectionResourceOverwrite",
103106
"GenericResourceOverwrite",
107+
"PropertyResourceOverwrite",
104108
"ResourceOverwrite",
105109
"ResourceOverwriteParser",
106110
"ResourceOverwritesContext",

packages/uipath-platform/src/uipath/platform/common/_bindings.py

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,8 +80,23 @@ def folder_identifier(self) -> str:
8080
return self.folder_key
8181

8282

83+
class PropertyResourceOverwrite(ResourceOverwrite):
84+
resource_type: Literal["property"]
85+
values: dict[str, str] = Field(alias="values")
86+
87+
model_config = ConfigDict(populate_by_name=True, extra="ignore")
88+
89+
@property
90+
def resource_identifier(self) -> str:
91+
return ""
92+
93+
@property
94+
def folder_identifier(self) -> str:
95+
return ""
96+
97+
8398
ResourceOverwriteUnion = Annotated[
84-
Union[GenericResourceOverwrite, ConnectionResourceOverwrite],
99+
Union[GenericResourceOverwrite, ConnectionResourceOverwrite, PropertyResourceOverwrite],
85100
Field(discriminator="resource_type"),
86101
]
87102

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
import json
2+
import logging
3+
from pathlib import Path
4+
from typing import Optional, Union, overload
5+
6+
from ._bindings import PropertyResourceOverwrite, _resource_overwrites
7+
from ._config import UiPathConfig
8+
9+
logger = logging.getLogger(__name__)
10+
11+
12+
class BindingsService:
13+
"""Service for reading Property binding resources from bindings.json.
14+
15+
Provides access to connector-defined property values (e.g. SharePoint folder IDs)
16+
that are configured at design time and resolved at runtime.
17+
"""
18+
19+
def __init__(self, bindings_file_path: Optional[Path] = None) -> None:
20+
self._bindings_file_path = bindings_file_path or UiPathConfig.bindings_file_path
21+
22+
def _load_bindings(self) -> list[dict]:
23+
try:
24+
with open(self._bindings_file_path, "r") as f:
25+
data = json.load(f)
26+
return data.get("resources", [])
27+
except FileNotFoundError:
28+
logger.debug("Bindings file not found: %s", self._bindings_file_path)
29+
return []
30+
except (json.JSONDecodeError, OSError) as e:
31+
logger.warning("Failed to load bindings file %s: %s", self._bindings_file_path, e)
32+
return []
33+
34+
def _find_property_resource(self, key: str) -> Optional[dict]:
35+
"""Find a Property binding resource by exact or suffix key match."""
36+
resources = self._load_bindings()
37+
for resource in resources:
38+
if resource.get("resource", "").lower() != "property":
39+
continue
40+
resource_key = resource.get("key", "")
41+
if resource_key == key or resource_key.endswith(f".{key}") or resource_key.endswith(key):
42+
return resource
43+
return None
44+
45+
def _get_overwrite(self, key: str) -> Optional[PropertyResourceOverwrite]:
46+
"""Check context var for a runtime overwrite for the given key."""
47+
context_overwrites = _resource_overwrites.get()
48+
if context_overwrites is None:
49+
return None
50+
# Try both "property.<key>" and the raw key
51+
for lookup_key in (f"property.{key}", key):
52+
overwrite = context_overwrites.get(lookup_key)
53+
if isinstance(overwrite, PropertyResourceOverwrite):
54+
return overwrite
55+
return None
56+
57+
@overload
58+
def get_property(self, key: str) -> dict[str, str]: ...
59+
60+
@overload
61+
def get_property(self, key: str, sub_property: str) -> str: ...
62+
63+
def get_property(
64+
self, key: str, sub_property: Optional[str] = None
65+
) -> Union[str, dict[str, str]]:
66+
"""Get the value(s) of a Property binding resource.
67+
68+
Args:
69+
key: The binding key, e.g. ``"sharepoint-connection.SharePoint Invoices folder"``.
70+
Accepts the full key or a suffix that uniquely identifies the binding.
71+
sub_property: The name of a specific sub-property to retrieve (e.g. ``"ID"``).
72+
If omitted, returns all sub-properties as a ``{name: value}`` dict.
73+
74+
Returns:
75+
The ``defaultValue`` of the requested sub-property when ``sub_property`` is
76+
given, or a dict of all sub-property names mapped to their ``defaultValue``
77+
when ``sub_property`` is omitted.
78+
79+
Raises:
80+
KeyError: When the binding key is not found, or when ``sub_property`` is given
81+
but does not exist on the binding.
82+
"""
83+
# Check for runtime overwrite first
84+
overwrite = self._get_overwrite(key)
85+
if overwrite is not None:
86+
if sub_property is not None:
87+
if sub_property not in overwrite.values:
88+
raise KeyError(
89+
f"Sub-property '{sub_property}' not found in Property binding '{key}'. "
90+
f"Available: {list(overwrite.values.keys())}"
91+
)
92+
return overwrite.values[sub_property]
93+
return dict(overwrite.values)
94+
95+
# Fall back to bindings.json
96+
resource = self._find_property_resource(key)
97+
if resource is None:
98+
raise KeyError(
99+
f"Property binding '{key}' not found in {self._bindings_file_path}."
100+
)
101+
102+
value: dict = resource.get("value", {})
103+
all_values = {name: props.get("defaultValue", "") for name, props in value.items()}
104+
105+
if sub_property is not None:
106+
if sub_property not in all_values:
107+
raise KeyError(
108+
f"Sub-property '{sub_property}' not found in Property binding '{key}'. "
109+
f"Available: {list(all_values.keys())}"
110+
)
111+
return all_values[sub_property]
112+
113+
return all_values

packages/uipath/specs/bindings.spec.md

Lines changed: 143 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ The configuration supports multiple resource types:
4343
4. **index** - Search indexes
4444
5. **apps** - Action center apps
4545
6. **connection** - External connections
46+
7. **Property** - Connector-defined resource properties (e.g. SharePoint folder IDs selected at design time)
4647

4748

4849
---
@@ -53,7 +54,7 @@ Each resource in the `resources` array has the following structure:
5354

5455
```json
5556
{
56-
"resource": "asset|process|bucket|index|connection",
57+
"resource": "asset|process|bucket|index|connection|Property",
5758
"key": "unique_key",
5859
"value": { ... },
5960
"metadata": { ... }
@@ -64,7 +65,7 @@ Each resource in the `resources` array has the following structure:
6465

6566
| Property | Type | Required | Description |
6667
|----------|------|----------|-------------|
67-
| `resource` | `string` | Yes | Resource type (one of the five types) |
68+
| `resource` | `string` | Yes | Resource type (one of the seven types) |
6869
| `key` | `string` | Yes | Unique identifier for this resource |
6970
| `value` | `object` | Yes | Resource-specific configuration |
7071
| `metadata` | `object` | No | Additional metadata for the binding |
@@ -303,6 +304,88 @@ Connections define external system integrations.
303304

304305
---
305306

307+
### 7. Property
308+
309+
Property bindings represent connector-defined resources that a user browses and selects at design time (e.g. a SharePoint folder, an OneDrive file). They are child resources of a parent Connection binding and contain **arbitrary sub-properties** with resolved values.
310+
311+
**Key Format:** `<parent-connection-uuid>.<label>`
312+
313+
**Example:**
314+
315+
```json
316+
{
317+
"resource": "Property",
318+
"key": "775694d9-4c5b-430f-bf47-6079b0ce8623.SharePoint Invoices folder",
319+
"value": {
320+
"FullName": {
321+
"defaultValue": "Invoices",
322+
"isExpression": false,
323+
"displayName": "File or folder",
324+
"description": "Select a file or folder",
325+
"propertyName": "BrowserItemFriendlyName"
326+
},
327+
"ID": {
328+
"defaultValue": "017NI543GXSYR5TZEZOBHJQNL6I2H4VA3M",
329+
"isExpression": false,
330+
"displayName": "File or folder",
331+
"description": "The file or folder of interest",
332+
"propertyName": "BrowserItemId"
333+
},
334+
"ParentDriveID": {
335+
"defaultValue": "b!fFiPzsQBgk2xGTJUTRo5jryva9eCrqNPowK3pN2kXWKF90cVuHqnS4RUsG9j1cRt",
336+
"isExpression": false,
337+
"displayName": "Drive",
338+
"description": "The drive (OneDrive/SharePoint) of file or folder",
339+
"propertyName": "BrowserDriveId"
340+
}
341+
},
342+
"metadata": {
343+
"ActivityName": "SharePoint Invoices folder",
344+
"BindingsVersion": "2.1",
345+
"ObjectName": "CuratedFile",
346+
"DisplayLabel": "FullName",
347+
"ParentResourceKey": "Connection.775694d9-4c5b-430f-bf47-6079b0ce8623"
348+
}
349+
}
350+
```
351+
352+
**Property-Specific Metadata:**
353+
- `ParentResourceKey`: The key of the parent Connection resource (`"Connection.<uuid>"`)
354+
- `ObjectName`: The connector-defined object type (e.g. `"CuratedFile"`)
355+
- `DisplayLabel`: The sub-property used as the primary display value
356+
357+
**Reading Property bindings at runtime:**
358+
359+
Use `sdk.bindings.get_property()` to read resolved Property values:
360+
361+
```python
362+
from uipath import UiPath
363+
364+
sdk = UiPath()
365+
366+
# Get a single sub-property value
367+
folder_id = sdk.bindings.get_property(
368+
"775694d9-4c5b-430f-bf47-6079b0ce8623.SharePoint Invoices folder",
369+
"ID"
370+
)
371+
# → "017NI543GXSYR5TZEZOBHJQNL6I2H4VA3M"
372+
373+
# Get all sub-properties as a dict
374+
props = sdk.bindings.get_property(
375+
"775694d9-4c5b-430f-bf47-6079b0ce8623.SharePoint Invoices folder"
376+
)
377+
# → {"FullName": "Invoices", "ID": "017NI543...", "ParentDriveID": "b!fFiPz..."}
378+
```
379+
380+
The key argument also accepts a suffix — any trailing portion of the full key that uniquely identifies the binding:
381+
382+
```python
383+
# Equivalent to the full key above
384+
folder_id = sdk.bindings.get_property("SharePoint Invoices folder", "ID")
385+
```
386+
387+
---
388+
306389
## Value Object Structure
307390

308391
### For Assets, Processes, Buckets, Apps and Indexes
@@ -334,13 +417,31 @@ Connections define external system integrations.
334417
}
335418
```
336419

337-
### Property Definition Fields
420+
### For Property bindings
421+
422+
The keys and number of sub-properties are connector-defined and vary by connector activity. Each sub-property follows this structure:
423+
424+
```json
425+
{
426+
"<SubPropertyName>": {
427+
"defaultValue": "resolved_value",
428+
"isExpression": false,
429+
"displayName": "Human-readable label",
430+
"description": "Optional longer description",
431+
"propertyName": "ConnectorInternalPropertyName"
432+
}
433+
}
434+
```
435+
436+
### Sub-property Definition Fields
338437

339438
| Field | Type | Required | Description |
340439
|-------|------|----------|-------------|
341-
| `defaultValue` | `string` | Yes | The default value for this property |
440+
| `defaultValue` | `string` | Yes | The resolved value for this sub-property |
342441
| `isExpression` | `boolean` | Yes | Whether the value is a dynamic expression (usually `false`) |
343442
| `displayName` | `string` | Yes | Human-readable name shown in UI |
443+
| `description` | `string` | No | Optional longer description of the sub-property |
444+
| `propertyName` | `string` | No | Internal connector property name |
344445

345446
---
346447

@@ -352,11 +453,13 @@ Metadata provides additional context about the resource binding.
352453

353454
| Field | Type | Description | Applicable To |
354455
|-------|------|-------------|---------------|
355-
| `ActivityName` | `string` | Activity used to access the resource | asset, process, bucket, index |
456+
| `ActivityName` | `string` | Activity used to access the resource | asset, process, bucket, index, Property |
356457
| `BindingsVersion` | `string` | Version of the bindings schema | All resources |
357-
| `DisplayLabel` | `string` | Label format for display | asset, process, bucket, index |
458+
| `DisplayLabel` | `string` | Label format for display | asset, process, bucket, index, Property |
358459
| `Connector` | `string` | Type of connector | connection |
359460
| `UseConnectionService` | `string` | Whether to use connection service | connection |
461+
| `ObjectName` | `string` | Connector-defined object type | Property |
462+
| `ParentResourceKey` | `string` | Key of the parent Connection resource (`"Connection.<uuid>"`) | Property |
360463

361464
---
362465

@@ -487,6 +590,40 @@ Metadata provides additional context about the resource binding.
487590
"Connector": "Salesforce",
488591
"UseConnectionService": "True"
489592
}
593+
},
594+
{
595+
"resource": "Property",
596+
"key": "775694d9-4c5b-430f-bf47-6079b0ce8623.SharePoint Invoices folder",
597+
"value": {
598+
"FullName": {
599+
"defaultValue": "Invoices",
600+
"isExpression": false,
601+
"displayName": "File or folder",
602+
"description": "Select a file or folder",
603+
"propertyName": "BrowserItemFriendlyName"
604+
},
605+
"ID": {
606+
"defaultValue": "017NI543GXSYR5TZEZOBHJQNL6I2H4VA3M",
607+
"isExpression": false,
608+
"displayName": "File or folder",
609+
"description": "The file or folder of interest",
610+
"propertyName": "BrowserItemId"
611+
},
612+
"ParentDriveID": {
613+
"defaultValue": "b!fFiPzsQBgk2xGTJUTRo5jryva9eCrqNPowK3pN2kXWKF90cVuHqnS4RUsG9j1cRt",
614+
"isExpression": false,
615+
"displayName": "Drive",
616+
"description": "The drive (OneDrive/SharePoint) of file or folder",
617+
"propertyName": "BrowserDriveId"
618+
}
619+
},
620+
"metadata": {
621+
"ActivityName": "SharePoint Invoices folder",
622+
"BindingsVersion": "2.1",
623+
"ObjectName": "CuratedFile",
624+
"DisplayLabel": "FullName",
625+
"ParentResourceKey": "Connection.775694d9-4c5b-430f-bf47-6079b0ce8623"
626+
}
490627
}
491628
]
492629
}

packages/uipath/src/uipath/_cli/models/runtime_schema.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ class BindingResourceValue(BaseModelWithDefaultConfig):
1818
default_value: str = Field(..., alias="defaultValue")
1919
is_expression: bool = Field(..., alias="isExpression")
2020
display_name: str = Field(..., alias="displayName")
21+
description: str | None = Field(default=None, alias="description")
22+
property_name: str | None = Field(default=None, alias="propertyName")
2123

2224

2325
# TODO: create stronger binding resource definition with discriminator based on resource enum.

0 commit comments

Comments
 (0)