Background
When the 26.11 update notes came out, I have to say I haven’t been that excited in a while (yes, still a nerd). As some of you know, I built EPM Workbench, seeing the new Calculation Manager APIs being released I had a lot of ideas on how to improve the workflow.
Currently EPM Workbench utilizes some tricks within the Artifact Snapshots to move rule changes in & out of calculation manager. I built and refined as many groovy restrictions as I could so that I could validate rules as I was typing, very similar to current modern day IDE. I was never going to get 100% accuracy but I could catch all the big & common items so that development would be more efficient.
So I spent the weekend with Claude and a throw-away test pod and mapped out how these REST APIs could be utilized. It ran about ~2,000+ test requests with different variations trying to figure out what is & isn’t possible. Short answer: some of it is great and I will use in EPM Workbench… some of it is not quite there yet and some I was able to find workarounds for. Below is everything I found, with details around how each was tested (including endpoints / payloads / parameters etc).
TLDR table
| Task | Verdict | Why |
|---|---|---|
| Read Rules and Rulesets | Works | Reading was an exact mirror and worked very well. As we made updates it was immediately reflected. |
| Validate Plain Text | Works… with a caveat | You can just pass any text in (doesn’t have to be a rule) and it validates it without saving/modifying anything. Issue is around %Templates/%Scripts (more detail below) |
| Validate an Existing Rule | Works… for some | If your rules contain any %Template/%Script references it fails… |
| Save a Rule (create or replace) | Works, with data loss | Works well for the major items… does drop some fields (more detail below) |
| Deploy a Rule | Works, but damages the Rule | Expands macros correctly, but strips data from the saved Rule |
| Delete Rules and Rulesets | Works | You have to know if it is deployed or not to run properly |
| Templates and Scripts | No API | No read, write, validate or delete |
| Shared prompts on their own | No API | They only travel inside a Rule |
I did not run anything with graphical rules as I generally don’t use them
My take: For a “v1” of the API, it does do a lot of things great. Validate with a few workarounds works wonders! Until we get some other items supported though it will be hard to use (i.e. I use a lot of %Templates )
The Basics
The routes
Everything lives under one base path:
<base-url>/HyperionPlanning/rest/v3/applications/{application}/calcmanager
| Method and path | Purpose |
|---|---|
GET /rules?planType={{planType}} | List Rules, with their prompts |
GET /rules?planType={{planType}}&name={{name}} | Filter the list by name |
GET /rules/{{name}}?planType={{planType}} | Get one Rule |
POST /rules | Create a Rule |
PUT /rules/{{name}} | Replace a Rule |
DELETE /rules/{{name}}?planType={{planType}}&deleteFrom=... | Delete a Rule |
POST /rules/validate?mode=... | Validate a Rule sent in the body |
POST /rules/{{name}}/validate?mode=...&planType={{planType}} | Validate a saved Rule |
POST /rules/{{name}}/deploy?planType={{planType}} | Deploy a saved Rule |
/rulesets/... | The same set of routes for Rulesets |
{{name}} in this case refers to the rule/ruleSet name.
The package format
Every read and every write uses the same shape, which Oracle calls a package. Think of it as a Rule plus the prompts it needs, all in one JSON body.
{
"primaryArtifact": { "type": "RULE", "name": "Demo-API-CalcScript", "planType": "FINANCE" },
"variables": [
{
"name": "RTP_FORECAST",
"scope": { "level": "RULE", "planType": "FINANCE", "ruleName": "Demo-API-CalcScript" },
"dataType": "MEMBER",
"promptText": "Select Forecast",
"useLastValue": false,
"dimension": "Scenario",
"limits": { "kind": "EXPRESSION", "expression": "Descendants(ForecastScenarios)" }
}
],
"rules": [
{
"name": "Demo-API-CalcScript",
"planType": "FINANCE",
"script": {
"language": "CALCSCRIPT",
"source": "FIX ({RTP_FORECAST})\n \"Acct100\" = #MISSING;\nENDFIX"
},
"variableReferences": [
{
"sequence": 1,
"variableName": "RTP_FORECAST",
"hidden": false,
"useAsOverrideValue": false,
"validationValue": "AprForecast"
}
]
}
],
"rulesets": []
}
Important difference here between “variables” and “variableReferences”:
variablesis the prompt definition. It is shared by every Rule that uses the prompt.variableReferencesholds the per-Rule settings: validation value, override value and hidden flag etc.
A Ruleset package looks like this:
{
"primaryArtifact": { "type": "RULESET", "name": "RS_Demo" },
"variables": [],
"rules": [
{
"name": "Demo-API-Groovy",
"planType": "FINANCE",
"script": { "language": "GROOVY", "source": "println \"Hello World!\"" },
"executionOptions": {},
"variableReferences": []
},
{
"name": "Demo-API-CalcScript",
"planType": "FINANCE",
"script": { "language": "CALCSCRIPT", "source": "SET UPDATECALC OFF;" },
"executionOptions": {},
"variableReferences": []
}
],
"rulesets": [
{
"name": "RS_Demo",
"mergeVariables": true,
"steps": [
{ "sequence": 1, "type": "RULE", "targetName": "Demo-API-Groovy", "targetPlanType": "FINANCE" },
{ "sequence": 2, "type": "RULE", "targetName": "Demo-API-CalcScript", "targetPlanType": "FINANCE" }
],
"variableReferences": []
}
]
}
List/Reading Rules
GET .../calcmanager/rules?planType={{planType}}
GET .../calcmanager/rules/{{name}}?planType={{planType}}
This one just works. I ran a lot of different variations/tests and it always came back matching. Great for quickly getting details on what rules are out there and what they do. One thing… this won’t tell if the rule is deployed.
Validate
This is the feature I was most excited about, and it semi-delivered. Two paths here to validate… you can validate an existing rule or you can pass the rule “package” in to validate (and no rule needs to exist).
There are a few different “Validation Modes”
| Mode | Description |
|---|---|
| VALIDATE_ONLY | Just runs a validation, does not save / alter the rule in anyway |
| VALIDATE_AND_DEPLOY | If Validation is successful it will deploy. |
Validating Existing Rule
POST ...calcmanager/rules/{{ruleName}}/validate?mode=VALIDATE_ONLY&planType={{planType}}
Response:
{
"mode": "VALIDATE_ONLY",
"status": "success",
"deployed": false,
"rules": [
{
"name": "Demo-API-CalcScript",
"application": "acmeplan",
"planType": "FINANCE",
"status": "success"
}
]
}
You might think the “deployed” will show if its deployed or not… Unfortunately no that is just populated if you do “VALIDATE_AND_DEPLOY” (see below)
POST ...calcmanager/rules/{{ruleName}}/validate?mode=VALIDATE_AND_DEPLOY&planType={{planType}}
{
"mode": "VALIDATE_AND_DEPLOY",
"status": "success",
"deployed": true,
"deployedArtifacts": [
{
"id": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
"name": "Demo-API-CalcScript"
}
],
"rules": [
{
"name": "Demo-API-CalcScript",
"application": "acmeplan",
"planType": "FINANCE",
"status": "success"
}
]
}
Validating Plain Text / Rule Updates
This is generally where I will use this endpoint. It allows you to run a validation on “freeform” text. So you pass in the whole rule content into “source” parameter and you provide the RTPs and it will validate it. The drawback is on %Templates / %Scripts. It doesn’t seem to like those yet (as of this round of testing).
This would not be easy to build by hand (especially if you have RTPs in there), so I am assuming your AI tool of choice will be building the payload!
In EPM Workbench we will use a workaround and just substitute the full content of the %Template/%Script into the rule and validate it that way. It’s not great but it works. Hopefully templates & scripts get support in the near future. Probably should be said if you are using an agent and they want to validate and the replace the %Template/%Script inline… don’t run VALIDATE_AND_DEPLOY as it won’t use the template.
POST .../calcmanager/rules/validate?mode=VALIDATE_ONLY
{
"primaryArtifact": { "type": "RULE", "name": "Demo-API-CalcScript", "planType": "FINANCE" },
"variables": [],
"rules": [{
"name": "Demo-API-CalcScript",
"planType": "FINANCE",
"script": {
"language": "CALCSCRIPT",
"source": "FIX (\"Jan\")\n \"NoSuchMember\" = 1;\nENDFIX"
},
"variableReferences": []
}],
"rulesets": []
}
Response:
{
"mode": "VALIDATE_ONLY",
"status": "error",
"deployed": false,
"rules": [{
"name": "Demo-API-CalcScript",
"application": "acmeplan",
"planType": "FINANCE",
"status": "error",
"errors": [{ "message": "The member 'NoSuchMember' cannot be found.", "lineNumber": 2 }]
}]
}
And a pass:
{
"mode": "VALIDATE_ONLY",
"status": "success",
"deployed": false,
"rules": [{ "name": "Demo-API-CalcScript", "application": "acmeplan", "planType": "FINANCE", "status": "success" }]
}
When trying to validate something with a template:
POST .../calcmanager/rules/validate?mode=VALIDATE_ONLY
Body:
{
"primaryArtifact": { "type": "RULE", "name": "Demo-API-CalcScript", "planType": "FINANCE" },
"variables": [],
"rules": [{
"name": "Demo-API-CalcScript",
"planType": "FINANCE",
"script": {
"language": "CALCSCRIPT",
"source": "SET AGGMISSG ON;\n%Template(name:=\"zT_FIN-Test\",application:=\"ACMEPLAN\",plantype:=\"FINANCE\",dtps:=())"
},
"variableReferences": []
}],
"rulesets": []
}
For awareness the template just has SET UPDATECALC OFF; in it
As you can see in the response below, it treats the params like member names… Response:
{
"mode": "VALIDATE_ONLY",
"status": "error",
"deployed": false,
"rules": [
{
"name": "Demo-API-CalcScript",
"application": "ACMEPLAN",
"planType": "FINANCE",
"status": "error",
"errors": [
{ "message": "The member 'zT_FIN-Test' cannot be found.", "lineNumber": 2 },
{ "message": "The member 'ACMEPLAN' cannot be found.", "lineNumber": 2 },
{ "message": "The member 'FINANCE' cannot be found.", "lineNumber": 2 }
]
}
]
}
I tried on Groovy rules too and still wouldn’t validate. I tried a lot of different variations to escape things etc… Maybe I didn’t find the secret and it is possible, but I couldn’t find it (i.e. Claude couldn’t).
Calc Scripts & Groovy Return Differently
| Calc script | Groovy | |
|---|---|---|
| Errors returned | Several at once | First error only |
| Line number | lineNumber on most errors | Empty, it’s only in the message text |
A Groovy failure looks like this, so you have to pull the line and column out of the message yourself:
{
"message": "The Groovy script failed to compile with internal error: Compile Error:\nUnexpected input: '=' @ line 3, column 9."
}
A regex like /@ line (\d+), column (\d+)/ does the job.
Saving Rules
Create with a POST and replace with a PUT, both using the package format above.
POST .../calcmanager/rules
PUT .../calcmanager/rules/{{name}}
The good news: Templates/Scripts survive. Your %Template stays as written, and EPM pulls in the Template’s content on save.
The not-so-good news: A PUT deletes the Rule’s description and comment, because there’s no JSON field for them. Which one could argue is non-overly used, but with the explosion of AI here, including some context in the description/comments of a rule would be very helpful.
Prompts are where saving gets risky:
| What you send | What happens |
|---|---|
| POST with a prompt definition that already exists | 400 with ["Variable","acmeplan.FINANCE.RTP_SCENARIO","Exists"] |
POST with {RTP_SCENARIO} in the source and no definition | Works. Then PUT to add the per-Rule settings |
| PUT with a definition and references | Works, but just be aware it updates the shared definition for every Rule that uses it |
PUT with no variables and no variableReferences | The shared prompt is safe, but this Rule loses its validation and override values |
| References with no definition | 400 Variable RTP_XYZ referenced by Rule is not defined in variables[]. |
So the path forward I think here, assuming the RTP already exists, is to POST to create the rule, have {RTP_SCENARIO} in the rule. Then you can immediately either GET and update with a PUT or just PUT to fill in the rule details for the prompt.
On top of that, JSON has no field for a prompt’s description or system default (i.e. to get the user name), so any PUT that carries the definition loses both. And there’s no prompt endpoint at all.
Deploy: Fast, Correct, and Destructive
POST .../calcmanager/rules/Demo-API-CalcScript/deploy?planType=FINANCE
No body needed. The good part is real: Oracle expands %Template, %Script and design-time prompts for you, correctly.
Two things to know first:
- A plain deploy doesn’t validate. It happily deployed a broken Rule.
- Similar to Calc Manager, a shared Template only updates the Rules you redeploy. After editing a Template and deploying Rule A, Rule B kept running the old Template.
Now the bad part. Deploying through the API also clears the description/comments etc. I tested a Rule with a description, a comment, and a hidden prompt that had a description and a system default:
| Step | Rule description | Comment | Prompt description | System default | Launches? |
|---|---|---|---|---|---|
| After API deploy | Removed | Removed | Removed | Removed | No |
The launch failure:
{
"descriptiveStatus": "Error",
"status": 1,
"jobId": -1,
"details": "This rule cannot be launched because it has hidden rtp variables without default values."
}
I tried pretty hard to get around this. 395 guessed field names across every part of the package all came back not recognized. Every way of triggering a deploy (plain deploy, validate-and-deploy, a Ruleset deploying its steps) strips the same data. Faking a system default with defaultValue: "user_name" just gives the Rule the literal text user_name.
Templates and Scripts
There are no routes for them. type=TEMPLATE is ignored, calcmanager/templates and /scripts return 404, and DELETE /rules/{template} returns 404. I tried a lot of different combinations of things to handle templates / scripts but could not find a viable path forward.
Deleting
DELETE .../calcmanager/rules/Demo-API-CalcScript?planType=FINANCE&deleteFrom=CALC_MANAGER
Returns 204 with no body. The deleteFrom parameter matters more than you’d think:
deleteFrom | Result |
|---|---|
CALC_MANAGER (the default) | Removes the saved Rule. The deployed copy stays and still runs |
PLANNING | Removes the deployed copy. The saved Rule stays |
ALL | Removes both, but returns 404 and deletes nothing if either side is missing |
So What Can You Use Today?
Here’s my recipe:
- Validate local text with
VALIDATE_ONLY. Have the agent expand templates/script inline when sending validations. - Read Rules and Rulesets freely.
- Delete Rules and Rulesets, after checking what exists on both sides.
What I’d Ask Oracle For
If you wanted my wish list of next updates:
- Enable Templates & Scripts!
- Provide additional fields for descriptions / comments (rules and prompts)
- A route to list run-time prompts.
lineNumberon Groovy errors, and all errors instead of just the first.- A deployed flag on GET.
This is a really promising start from Oracle and I am excited to explore a lot of the other REST APIs. Validate alone saves a lot of back and forth.