Skip to content

feat(business-rules): add BusinessRulesService with run() - #1912

Open
ashishupadhyay88 wants to merge 13 commits into
mainfrom
feat/business-rules-evaluate
Open

ashishupadhyay88 wants to merge 13 commits into
mainfrom
feat/business-rules-evaluate

Conversation

@ashishupadhyay88

@ashishupadhyay88 ashishupadhyay88 commented Sep 28, 2026 •

Copy link
Copy Markdown

Summary

Jira: MST-15365

Adds sdk.business_rules, a client for the Business Rules service. It runs a business rule deployed to Orchestrator against one input.

This is PR 1 of 2. #1917 adds debug runs of undeployed rules in Studio projects.

from uipath.platform import UiPath
from uipath.platform.business_rules import TraceContext

result = UiPath().business_rules.run(
    "Loan Pricing",                  # rule name first, like processes.invoke(name, input_arguments)
    {"creditScore": 740},
    version="1.0.3",                 # optional; active version if omitted
    folder_path="Finance",           # or folder_key=..., or organization_unit_id=..., or UIPATH_FOLDER_KEY / _PATH
    explain=True,                    # optional; needs a folder key
    trace_context=TraceContext(trace_id="…", parent_span_id="…"),  # optional; automatic if omitted
)
result.status                 # Success | PartialSuccess | AllFailed
result.decisions[0].outputs   # {"Grade": "B", "Rate": 3.5}

run_async() has the same signature, for callers already running on an event loop, such as LangGraph or FastAPI.

Flow

flowchart TD
    A["run(name, input) / run_async(name, input)"] --> O{"businessRule binding<br/>override for name?"}
    O -- yes --> O1["name and folder replaced<br/>(binding folder wins over folder_key)"] --> V
    O -- no --> V{"name safe, input a mapping<br/>with at most 256 keys?"}
    V -- no --> X["ValueError, nothing sent"]
    V -- yes --> F{"folder"}
    F -- "folder_key and folder_path" --> X
    F -- "folder_key" --> K["folder key"]
    F -- "folder_path" --> L["FolderService lookup"] --> K
    F -- "organization_unit_id only" --> U["numeric id, no env fallback"]
    F -- "nothing: UIPATH_FOLDER_KEY / _PATH" --> K
    F -- "nothing at all" --> X
    K --> EX{"explain=True?"}
    U --> EX
    EX -- "yes, no folder key" --> X
    EX -- ok --> EV["POST …/businessrules_/v1/business-rules/evaluate<br/>x-uipath-folderkey if known, x-uipath-organizationunitid if given<br/>businessRuleName, version?, decisionNames?, explain,<br/>inputs: [id: input-1, data]"]
    EV --> T{"trace_context?"}
    T -- yes --> T1["x-uipath-traceparent-id = explicit"]
    T -- no --> T2["x-uipath-traceparent-id = UIPATH_TRACE_ID / current span"]
    T1 --> BS["BaseService: bearer token, /org/tenant URL, retries"]
    T2 --> BS
    BS --> R{"HTTP status"}
    R -- "4xx / 5xx" --> E["EnrichedException"]
    R -- "200 / 207" --> S["result for input-1 → status<br/>Success / PartialSuccess / AllFailed"]
Loading

Design

The request and result behaviour follows the .NET client in business-rules/clients/dotnet. The call shape follows this SDK's resource conventions. The SDK checks only what's required; optional values the caller passes are sent as given.

  • Name first, like every resource call. run(name, input, *, version=None, …) matches processes.invoke(name, input_arguments). The endpoint and the request builder (_evaluate_spec) are private.

  • Folder, in any of three forms:

    • folder_key, sent as is
    • folder_path, looked up through FolderService and sent as its key
    • organization_unit_id, the numeric folder id, sent as is as x-uipath-organizationunitid

    The service accepts either the key or the numeric id and prefers the key (business-rules cli: langchain enable traces #55); both are sent when both are given.

    • Required: one of the three, or the env (UIPATH_FOLDER_KEY, then UIPATH_FOLDER_PATH). With explain=True, a folder key is required; the numeric id alone isn't enough.
    • Exclusive: folder_key and folder_path together are a ValueError, as in header_folder() across the SDK.
    • Env fallback only fills a gap: it's not added when the caller named the folder by its numeric id, except to supply the key explain=True needs.
  • Resource overrides. A solution's businessRule bindings can remap the rule name and folder per environment, through a private helper decorated with @resource_override(resource_type="businessRule"). When a binding remaps the rule, its folder replaces the caller's, including a folder_key.

  • Single input in, decisions out. The service's batch format stays internal. The SDK sends one input with id input-1 and returns only its result as a BusinessRuleRunResult, stamped with RunMode.DEPLOYED.

  • Checks before sending:

    • rule names at most 256 characters, without /, \, .., % or control characters
    • input a mapping with at most 256 keys
  • Status. An input-level error means AllFailed. Otherwise the status comes from how many decisions failed. A 207 partial result is returned normally, with top_level_error such as BATCH_TIMEOUT. Error responses raise EnrichedException.

  • Trace header.

    • x-uipath-traceparent-id is automatic by default, from UIPATH_TRACE_ID and the current span.
    • Callers can override it with trace_context=TraceContext(trace_id, parent_span_id), the same shape as .NET's, with the ids checked when it's created.
    • The explicit value travels in the headers handed to BaseService, as a small dict that ignores BaseService's later write of the ambient header. There are no request hooks and no shared-code change, and it survives retries.
  • Inherited from BaseService:

    • bearer auth, including S2S
    • URL scoping
    • retries on timeouts and 408, 429, 502, 503, 504, 524
    • UIPATH_SERVICE_URL_BUSINESSRULES local override

For reviewers

  • Account and tenant headers. The SDK doesn't send x-uipath-internal-accountid / -tenantid; the gateway adds them, confirmed on alpha.
  • Shared code. The only change is "businessRule" in GenericResourceOverwrite (common/_bindings.py), as feat: add memorySpace to resource overwrite types #1586 / fix: accept remoteA2aAgent in GenericResourceOverwrite #1581 did for their kinds.
  • Version bump. uipath-platform goes from 0.2.32 to 0.2.33. uv lock --check passes for both packages.
  • Review feedback addressed:
    • name-first signature and @resource_override
    • a binding's folder replacing folder_key
    • non-dict input rejected
    • "business rules" rather than DMN
    • Sonar S5778 and S7503
    • Copilot's UIPATH_FOLDER_PATH tests

Test plan

  • 53 tests in tests/services/test_business_rules_service.py, covering:
    • request body and headers
    • folder key, path, numeric id, and their combinations (both sent; the env not replacing an explicit numeric id; explain needing a key)
    • the env fallbacks and their order
    • input checks
    • each status, 207 responses and error responses
    • explicit vs automatic trace (no leak, survives a retry)
    • businessRule overrides (including over folder_key)
    • run_async
  • The full uipath-platform suite passes: 1800 passed, 7 skipped (live credentials). The uipath override and bindings tests also pass.
  • ruff check, ruff format --check and mypy src tests are clean in the locked uv environment.
  • Real alpha, from a Coded Function (uip function run), with the combined branch (feat(business-rules): add debug runs to run() #1917): 60 passed, 0 failed.
    • every evaluate case with explain off and on, checked on the wire
    • the numeric id alone (only x-uipath-organizationunitid sent → Success)
    • key + numeric id; path + numeric id with explain; the numeric id with a different env folder (env not added)
    • the numeric id alone with explain=True → ValueError

🤖 Generated with Claude Code

Adds sdk.business_rules, a client for the Business Rules service that
evaluates a DMN business rule deployed to Orchestrator against one input.

- evaluate / evaluate_async: single input in, decisions out; the
  service's batch contract stays internal, matching the .NET client
- folder scoping by folder_key or folder_path (resolved to a key, as the
  service accepts keys only), falling back to UIPATH_FOLDER_KEY/PATH
- client-side validation of rule name and input size, mirroring the
  service and the .NET client
- overall status (Success / PartialSuccess / AllFailed) derived from
  decision- and input-level errors; 207 partial results are returned,
  error envelopes raise EnrichedException
- auth, retry, tenant URL scoping and trace propagation are inherited
  from BaseService

Bumps uipath-platform to 0.2.33.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Copilot AI lite review requested due to automatic review settings September 28, 2026 04:18
@github-actions github-actions Bot added test:uipath-langchain Triggers tests in the uipath-langchain-python repository test:uipath-integrations labels Sep 28, 2026

Copilot AI left a comment

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.

Copilot review overview

🟡 Changes recommended

Fix the folder key/path presence check and add coverage for the documented UIPATH_FOLDER_PATH fallback.

Review effort: Lite
Findings: 1 Low severity

Open (1)
What changed in this PR

Adds Business Rules evaluation support to uipath-platform, including sync/async APIs, validation, folder resolution, and response handling.

Changes:

  • Added Business Rules models and service client.
  • Exposed UiPath.business_rules.
  • Added tests, documentation, and version updates.
File Reviewed changes
packages/​uipath/​uv.lock Dependency lock update
packages/​uipath-platform/​uv.lock Platform version lock update
packages/​uipath-platform/​tests/​services/​test_business_rules_service.py Service behavior tests
packages/​uipath-platform/​src/​uipath/​platform/​business_rules/​business_rules.py Evaluation models and statuses
packages/​uipath-platform/​src/​uipath/​platform/​business_rules/​_business_rules_service.py Evaluation service, validation, folder resolution, and response mapping
packages/​uipath-platform/​src/​uipath/​platform/​business_rules/​__init__.py Public API exports
packages/​uipath-platform/​src/​uipath/​platform/​_uipath.py business_rules client integration
packages/​uipath-platform/​pyproject.toml Package version bump
packages/​uipath-platform/​CLAUDE.md Service documentation

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

@ashishupadhyay88
ashishupadhyay88 marked this pull request as draft September 28, 2026 05:18
Replaces the public evaluate()/evaluate_async() with run()/run_async(),
modelled on the .NET client's RunAsync: the caller passes a run context
(DeployedRunContext(rule_name, version)) and never picks an endpoint.
The result is BusinessRuleRunResult, stamped with the RunMode that ran.

The evaluate request builder stays private, so a debug run context can
be added later without changing the public entry point.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@ashishupadhyay88 ashishupadhyay88 changed the title feat(business-rules): add BusinessRulesService with evaluate feat(business-rules): add BusinessRulesService with run() Sep 29, 2026
@ashishupadhyay88
ashishupadhyay88 marked this pull request as ready for review September 29, 2026 02:27
@ashishupadhyay88

Copy link
Copy Markdown
Author

Tested against alpha

This tests run() / run_async() with DeployedRunContext, on commit 3795b306.

Setup

  • Tenant: alpha.uipath.com/bruleswe/DefaultTenant
  • Login: uipath auth --alpha (user token)
  • Folder: Shared/Solution_ashish1 (key 4973409f-…)
  • Rule: Business Rules
  • Input: {"age": 14}
# Call Result
1 run(..., deployed=DeployedRunContext(rule_name="Business Rules"), folder_path=...) ✅ Success: {"category": "minor"}, service reported version 1.1.4
2 same + version="1.1.4" ✅ Success, same output
3 same + explain=True ✅ Success, same output (no 400: folder key sent)
4 version="1.1.4" + explain=True ✅ Success, same output
5 run_async(...), same as 1 ✅ Success, same output
6 unknown rule name ✅ EnrichedException 404 RULE_NOT_FOUND (service's own error body)

What this confirms

  • Gateway headers. The request went through the real gateway: POST https://alpha.uipath.com/bruleswe/DefaultTenant/businessrules_/v1/business-rules/evaluate. The service returns 400 if the account or tenant header is missing, so the gateway adds x-uipath-internal-accountid / -tenantid, and the SDK doesn't need to send them. This settles the open reviewer question.
  • Folder path. folder_path was turned into a key and sent as x-uipath-folderkey.
  • Result handling. The single input was sent as input-1, and the result was picked out and mapped to mode: Deployed / status: Success.

Checks in the locked uv environment (Python 3.11)

  • uv lock --check passes for uipath-platform and uipath, so the hand-edited lockfile versions are consistent.
  • 20 Business Rules tests pass.
  • The full suite passes: 1767 passed, 7 skipped.
  • ruff check, ruff format --check and mypy src tests are clean.

ashishupadhyay88 and others added 3 commits September 29, 2026 09:35
- Add sync and async tests for the UIPATH_FOLDER_PATH env fallback,
  which resolves the path to a key before sending x-uipath-folderkey,
  and a test that UIPATH_FOLDER_KEY wins over UIPATH_FOLDER_PATH
  without a lookup (Copilot review).
- Build DeployedRunContext outside pytest.raises so each block has a
  single call that can raise (Sonar python:S5778).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…sync()

Callers can now pass TraceContext(trace_id, parent_span_id), like the
.NET client's TraceContext, to file the run's spans under a trace of
their choosing. When it is omitted the header stays automatic: the
trace from UIPATH_TRACE_ID and the current span, as for every service.

- TraceContext validates at construction: 32-hex (or UUID) trace id,
  16-hex parent span id, neither all zeros; ids are normalised.
- BaseService always sets the ambient header, so a request hook on this
  service's own sync and async httpx clients replaces it with the
  explicit value just before sending. Shared code is unchanged.
- The explicit value lives in a ContextVar for the duration of the
  call, so it never leaks into the next call or across concurrent
  async runs, and it survives retries.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

Copilot AI left a comment

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.

Copilot review overview

🔵 Needs a closer look

Address non-mapping input validation and malformed trace ID validation.

Review effort: Lite
Findings: None

Resolved since last review (1)

@NishankSiddharth NishankSiddharth left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

do we need to add get, list methods for business rules (listing BRs, getting details for BRs)?

Comment thread packages/uipath-platform/CLAUDE.md Outdated
Addresses review on #1912:

- run()/run_async() take the rule name as the first parameter, like
  processes.invoke(name, input_arguments):
  run("Loan Pricing", {"age": 14}, version=..., folder_path=...).
  DeployedRunContext is removed (never released); version is a keyword.
- @resource_override(resource_type="businessRule") on both, so a
  solution's bindings can remap the rule name and folder per
  environment. "businessRule" is the Studio resource kind.
- Add "businessRule" to GenericResourceOverwrite so such bindings parse,
  as done for memorySpace (#1586) and remoteA2aAgent (#1581).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ashishupadhyay88 added a commit that referenced this pull request Sep 29, 2026
Brings #1912's review fix (name-first run() with resource overrides)
into the debug PR and reshapes debug runs to match:

- run(name, input, *, debug=DebugRunContext(...)): the rule name is the
  first parameter for both modes, so bindings can remap it for debug
  runs too. DebugRunContext drops rule_name and keeps project_id,
  file_name, job_key and organization_unit_id.
- Without project_id, the run resolves the project from the job's
  lineage and needs job_key (or UIPATH_JOB_KEY) and organization_unit_id.
- version applies to deployed rules only and is rejected with debug.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ashishupadhyay88 and others added 2 commits September 29, 2026 18:50
Addresses review: describe the service in product terms in the package
guide and docstrings.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ashishupadhyay88 added a commit that referenced this pull request Sep 29, 2026
Brings #1912's wording change into the debug PR and rewords the debug
docstrings and field descriptions the same way. Literal .dmn file names
in examples and tests are unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Fixes Sonar python:S7503 on the async request hook, which had to be
`async` for httpx but awaited nothing.

The explicit trace_context is now carried in the headers passed to
BaseService: a small dict that ignores BaseService's later write of the
ambient trace header when an explicit one is set. This removes both
httpx request hooks, the async hook function and the ContextVar. Each
call gets its own headers, so nothing leaks between calls or across
concurrent async runs, and retries reuse them (new test). Shared code
is unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

Copilot AI left a comment

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.

Copilot review overview

🟡 Changes recommended

Three unresolved moderate findings remain in the Business Rules service implementation.

Review effort: Lite
Findings: 2 Medium severity

Open (2)

Comment on lines +180 to +181
if folder_key and folder_path:
raise ValueError("Only one of folder_key or folder_path can be provided")
Comment on lines +284 to +287
if input is None:
raise ValueError("input must not be None")
if len(input) > _MAX_INPUT_KEYS:
raise ValueError(f"input must not exceed {_MAX_INPUT_KEYS} keys")
ashishupadhyay88 and others added 2 commits September 30, 2026 09:10
…t non-dict input

Addresses Copilot review on #1912:

- A businessRule binding supplies its folder as a path. The override
  decorator on run() replaced folder_path but left the caller's
  folder_key, so run() saw both and raised "Only one of folder_key or
  folder_path". The binding is now applied by a private helper
  (_binding, decorated with resource_override); when it remaps the
  rule, its folder replaces whichever folder the caller gave, including
  folder_key. Sync and async tests cover it, plus a non-matching
  override keeping the caller's key.
- input must be a mapping: a list or string is now a ValueError before
  anything is sent, instead of being posted as inputs[0].data.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
run()/run_async() now take the folder three ways: folder_key (sent as
is), folder_path (looked up, sent as the key) or organization_unit_id
(sent as-is as x-uipath-organizationunitid). The service accepts the
numeric id for a deployed rule and prefers the key when both are sent.

- A folder is required in one of those forms; explain=True still needs
  a folder key, since the numeric id can't stand in for it.
- folder_key and folder_path stay exclusive, as elsewhere in the SDK.
- The environment's folder only fills a gap: it isn't added when the
  caller named the folder by its numeric id, except to supply the key
  that explain=True needs.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ashishupadhyay88 added a commit that referenced this pull request Sep 30, 2026
… debug too

Brings #1912's organization_unit_id into the debug PR and applies the
"require only what's needed, accept optional values" rule to debug runs:

- organization_unit_id moves from DebugRunContext to run(), next to
  folder_key / folder_path. Job-lineage mode requires it (and a job key,
  explicit or UIPATH_JOB_KEY); project mode needs no folder at all.
- Project mode no longer rejects a job key or folder id the caller
  passes: they're sent as given. UIPATH_JOB_KEY is still not added on its
  own, and businessRuleName is still never sent with projectId.
- version is sent with debug runs when given, instead of being rejected.
- explain=True needs a folder key in every mode.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ashishupadhyay88 and others added 2 commits September 30, 2026 10:23
main released 0.2.33 (#1920), so this PR moves to the next version.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ashishupadhyay88 added a commit that referenced this pull request Sep 30, 2026
#1912 now takes 0.2.34 (main released 0.2.33), so this PR moves to the
next version.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@sonarqubecloud

Copy link
Copy Markdown

@github-actions

Copy link
Copy Markdown

🚨 Heads up: uipath-langchain cross-tests are FAILING 🚨

Your changes may break the uipath-langchain-python integration.

⚠️ These checks are NOT enforced by branch protection rules. Please review the failures before merging.

🔍 Inspect the failed run →

@ashishupadhyay88

ashishupadhyay88 commented Sep 30, 2026 •

Copy link
Copy Markdown
Author

Tested in a coded agent on alpha (bruleswe/DefaultTenant)

I built a LangGraph coded agent that calls sdk.business_rules.run_async(...) from two LLM tools, run_business_rule (deployed) and debug_business_rule (debug, by project; debug is added in #1917). It ran against the unreleased SDK from #1917's TestPyPI dev build, which includes this PR (uipath-platform==0.2.35.dev1019177758). I deployed it to Orchestrator (personal workspace) and ran it as jobs.

Job Question Tool calls Result
0e3e9b0a… Category for a 78-year-old run_business_rule({"age": 78}) ✅ Deployed · Success · adult · 1.1.5
84c64b3a… Studio version, age 14, explain=true, no folder key passed debug_business_rule({"age": 14}) ✅ Debug · Success · minor · Business rule.dmn
5e4d9370… Compare 14, 35 and 78, explain=true 3 × run_business_rule ✅ Deployed · Success · minor / adult / adult

Local runs before deploying: 5/5 correct with uip codedagent run, and a 3-case smoke eval scored 3/3 (1.0).

What this shows:

  • In a job, the folder comes from the environment. In job 2, explain=true needs a folder key and the agent passed none. The SDK read UIPATH_FOLDER_KEY from the job and the service accepted it.
  • run_async works inside LangGraph, including several calls in one turn.
  • No extra auth setup. The robot token and the gateway-injected account and tenant headers were enough.

Not checked: nesting of the Business Rules span in the agent trace. uip or jobs traces only serves Agent-type processes.

Details: https://claude.ai/artifact/Tgr4kV4qg6T8K35yVGstN3

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

test:uipath-integrations test:uipath-langchain Triggers tests in the uipath-langchain-python repository

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants