Calling Internal EPM REST APIs from Groovy
Map Planning, interop, and /aif/ trees on your pod, then POST jobs and poll status from Groovy with a reusable callEpm helper.
Share on LinkedIn
Why this exists
With a named Connection in place, Groovy can call the REST trees on the same EPM pod: Planning jobs, interop / platform, and Data Integration pipelines.
An EPM pod is not one API. It is one host with several REST trees bolted onto it. Get the host right, pick the tree that actually owns the job, then version the path. Most 404s I have caused were "I called Planning for a Data Integration pipeline" or "I left /epmcloud on the URL."
The example in this post is a Groovy rule that runs a pipeline and awaits status until completed or failed. For other tasks see the full catalog: Oracle Fusion Cloud EPM REST APIs. Pick an endpoint there, then drop the path and payload into the Groovy config below.
Three trees you will actually hit
Discover versions before you hardcode them. Each tree tells you what it still serves:
GET /HyperionPlanning/rest/
GET /interop/rest/
GET /aif/rest/Planning answers with v1 / v2 (deprecated) and v3 (lifecycle: active, isLatest: true). Data Integration answers V1 — capital V, because of course it does.
| Tree | Prefix | Current version | What it is |
|---|---|---|---|
| Planning (and FCCS / TRCS sitting on Planning) | /HyperionPlanning/rest/{api_version}/ | v3 | Application jobs, subvars, documents, job definitions |
| Platform / Migration (EPM Automate's REST twin) | /interop/rest/{api_version}/ | prefer v2 | Snapshots, file inbox/outbox, copy between instances, maintenance, security |
| Data Integration / Data Management | /aif/rest/{api_version}/ | V1 | Integrations, pipelines, data rules, batches, mappings |
FCCS is not a fourth host. Consolidation jobs, supplemental data, and a lot of close automation still go through the Planning tree on that pod. Data loads still go through /aif/. LCM still goes through /interop/.
Planning: jobs, subvars, documents
Application-scoped. You need the application name in the path (FCCSPROD, EPMPLN, whatever Inbox Explorer shows — not the pretty display name).
GET /HyperionPlanning/rest/v3/applications
POST /HyperionPlanning/rest/v3/applications/{application}/jobs
GET /HyperionPlanning/rest/v3/applications/{application}/jobs/{jobId}
GET /HyperionPlanning/rest/v3/applications/{application}/jobdefinitions
GET /HyperionPlanning/rest/v3/applications/{application}/substitutionvariables
POST /HyperionPlanning/rest/v3/applications/{application}/substitutionvariables
GET /HyperionPlanning/rest/v3/applications/{application}/documentsPOST .../jobs is fire-and-forget. Body is jobType, jobName, parameters. The response hands you a job id. Poll GET .../jobs/{jobId} until it is not running. Same pattern as EPM Automate, just JSON.
Subvars exist at application and plan-type scope. If you write CurrYear at app level and the cube still shows the old value, you posted to the wrong collection.
Interop: files, snapshots, the stuff EPM Automate does
This is the platform. Copy a snapshot from prod to test, upload a file, list the inbox, kick daily maintenance — /interop/, not Planning.
GET /interop/rest/v2/files/list
POST /interop/rest/v2/files/upload
POST /interop/rest/v2/snapshots/copyfrominstance
POST /interop/rest/v2/jobsv1 paths still exist (/interop/rest/11.1.2.3.600/applicationsnapshots/...). They work until they do not. New work should be v2.
Data Integration: integrations, pipelines, data rules
One endpoint, jobType in the body decides what you ran:
POST /aif/rest/V1/jobs
GET /aif/rest/V1/jobs/{jobId}INTEGRATION, PIPELINE, DATARULE, batches, mapping import/export. Period names have to match Data Integration period maps, not the Planning Period member you use in a form. Status codes on this tree are numeric (-1 in progress, 0 success, 1 error). Do not reuse your Planning job-status parser.
What belongs in the Connection vs the rule
The Connection should be the host plus credentials. Maybe a root path if you want one Connection per tree. The rule owns the rest: /HyperionPlanning/rest/v3/..., application name, job payload, poll loop.
If you bake /HyperionPlanning/rest/v3 into the Connection, you will create a second Connection the first time you need /aif/rest/V1/jobs. That is a valid choice. Just make it on purpose.
Groovy: consume the named connection
One file for the pipeline walkthrough. Download RunPipeline.groovy or paste the block below. The same folder also has the close-period and EDMCS validation rules from Common use cases. Planning injects the rule into run(), so a nested method (HttpResponse callEpm(...) { }) will not save. A closure will.
jobName for a pipeline is the Pipeline code from Data Integration, not the pretty name. Codes are 3–30 alphanumeric characters and they do not change after create. Payload shape is in Run a Pipeline. Everything else is in the REST API catalog.
Create string runtime prompts on the rule with these names (Calc Manager → Variables): PIPELINE_NAME, PIPELINE_START_PERIOD, PIPELINE_END_PERIOD, PIPELINE_IMPORT_MODE, PIPELINE_EXPORT_MODE, PIPELINE_ATTACH_LOGS, PIPELINE_SEND_MAIL, PIPELINE_SEND_TO. PIPELINE_NAME is the Pipeline code.
Path is relative to BASE-URL (no /epmcloud). Connection has get/post/put/delete — no patch. Use JsonOutput, not json().
/* RTPS: {PIPELINE_NAME} {PIPELINE_START_PERIOD} {PIPELINE_END_PERIOD} {PIPELINE_IMPORT_MODE} {PIPELINE_EXPORT_MODE} {PIPELINE_ATTACH_LOGS} {PIPELINE_SEND_MAIL} {PIPELINE_SEND_TO} */
import groovy.json.JsonOutput
import groovy.json.JsonSlurper
// --- Boilerplate. Paste once. Do not edit. ---
// Connection has get/post/put/delete — no patch.
// Use JsonOutput.toJson, not json(). This is a closure on purpose:
// Planning injects the rule into run(), so a nested method will not save.
class EpmRestConfig {
String connectionName
String method
String path
Object payload
}
def callEpm = { EpmRestConfig cfg ->
def conn = operation.application.getConnection(cfg.connectionName)
String method = cfg.method.toUpperCase()
String jsonBody = cfg.payload == null ? null : JsonOutput.toJson(cfg.payload)
HttpResponse response
switch (method) {
case "GET":
response = conn.get(cfg.path).asString()
break
case "POST":
response = jsonBody == null
? conn.post(cfg.path).asString()
: conn.post(cfg.path)
.body(jsonBody)
.asString()
break
case "PUT":
response = jsonBody == null
? conn.put(cfg.path).asString()
: conn.put(cfg.path)
.body(jsonBody)
.asString()
break
case "DELETE":
response = conn.delete(cfg.path).asString()
break
default:
throw new IllegalArgumentException(
"Unsupported method: ${cfg.method}. Use GET, POST, PUT, or DELETE."
)
}
// response.status is HTTP. The Data Integration job status is in the JSON body.
println "status=${response.status} ${response.statusText} ${method} ${cfg.path}"
println response.body
if (response.status < 200 || response.status >= 300) {
throwVetoException("EPM REST ${method} ${cfg.path} failed: ${response.status} ${response.body}")
}
return response
}
// --- Custom. RTPs, submit, poll. ---
// Pipeline *code* from Data Integration, not the pretty name.
String pipeline = rtps.PIPELINE_NAME
String startPeriod = rtps.PIPELINE_START_PERIOD
String endPeriod = rtps.PIPELINE_END_PERIOD
String importMode = rtps.PIPELINE_IMPORT_MODE
String exportMode = rtps.PIPELINE_EXPORT_MODE
String attachLogs = rtps.PIPELINE_ATTACH_LOGS
String sendMail = rtps.PIPELINE_SEND_MAIL
String sendTo = rtps.PIPELINE_SEND_TO
// POST accepts the job. HTTP 200 is not "the pipeline finished."
EpmRestConfig cfg = new EpmRestConfig(
connectionName: "EPM_REST", // named Connection: host + Basic auth only
method: "POST",
path: "/aif/rest/V1/jobs", // capital V. /aif/rest/v1 will 404
payload: [
jobName: pipeline,
jobType: "pipeline", // swap to INTEGRATION / DATARULE for those jobs
variables: [
STARTPERIOD: startPeriod,
ENDPERIOD: endPeriod,
IMPORTMODE: importMode,
EXPORTMODE: exportMode,
ATTACH_LOGS: attachLogs,
SEND_MAIL: sendMail,
SEND_TO: sendTo
]
]
)
HttpResponse response = callEpm(cfg)
// parseText is Object to STC. Cast to Map and use get() — job.jobId will not save.
JsonSlurper slurper = new JsonSlurper()
Map job = slurper.parseText(response.body as String) as Map
String jobId = job.get("jobId").toString()
int jobStatus = Integer.parseInt(job.get("status").toString())
// 40 * 15s = 10 minutes. Size under the Groovy rule timeout. Keep the cap.
int pollMs = 15000
int maxPolls = 40
int polls = 0
while (jobStatus == -1 && polls < maxPolls) {
sleep(pollMs)
polls++
// Same connection. GET this job — do not POST another pipeline.
cfg = new EpmRestConfig(
connectionName: "EPM_REST",
method: "GET",
path: "/aif/rest/V1/jobs/${jobId}",
payload: null
)
response = callEpm(cfg)
job = slurper.parseText(response.body as String) as Map
jobStatus = Integer.parseInt(job.get("status").toString())
println "poll ${polls}/${maxPolls} jobId=${jobId} status=${jobStatus} ${job.get('jobStatus')}"
}
if (jobStatus == -1) {
throwVetoException("Pipeline ${pipeline} still running after ${maxPolls} polls (jobId=${jobId})")
}
if (jobStatus != 0) {
// 1 error, 2 cancel pending, 3 cancelled, 4 invalid parameter
throwVetoException("Pipeline ${pipeline} failed: status=${jobStatus} jobId=${jobId} ${response.body}")
}Period values are Data Integration period names (Jan-26), not Planning Period members. Out-of-box pipeline variables: STARTPERIOD, ENDPERIOD, IMPORTMODE, EXPORTMODE, ATTACH_LOGS, SEND_MAIL, SEND_TO. Drop a key if you are not prompting for it and want the Pipeline definition default.
The JSON status is a string ("-1" / "0" / "1"), so parse it with Integer.parseInt. JsonSlurper.parseText is Object to Planning's type checker — cast as Map and use get("jobId"), not job.jobId. 0 is success. Anything else (1 error, 3 cancelled, 4 invalid) vetoes. maxPolls * pollMs is the wait cap (40 × 15s = 10 minutes here) — size it under the Groovy rule timeout. Do not omit the cap.
Keep the same callEpm closure. Change only the custom EpmRestConfig for other catalog endpoints:
// GET versions
EpmRestConfig cfg = new EpmRestConfig(
connectionName: "EPM_REST",
method: "GET",
path: "/HyperionPlanning/rest/",
payload: null
)
callEpm(cfg)
// One-shot job status (the pipeline example has the wait loop)
cfg = new EpmRestConfig(
connectionName: "EPM_REST",
method: "GET",
path: "/aif/rest/V1/jobs/${jobId}",
payload: null
)
callEpm(cfg)
// Planning business rule
cfg = new EpmRestConfig(
connectionName: "EPM_REST",
method: "POST",
path: "/HyperionPlanning/rest/v3/applications/EPMPLN/jobs",
payload: [jobType: "Rules", jobName: "Agg_Plan"]
)
callEpm(cfg)Common use cases
These are a few situations where I ultimately determined that a Groovy rule was the best fit. They represent only a small sample of the possible use cases; there are many more opportunities to apply Groovy within Oracle Cloud EPM processes.
Ultimately, the decision comes down to two questions:
- Can the pipeline accomplish the required objective on its own?
- If not, is there an available Oracle EPM REST API endpoint that can support the requirement?
YTD FCCS Multi Period load for historicals
I decided to implement a Groovy script because, when loading YTD data into FCCS, the closing balance must be calculated through consolidation after each period.
My initial thought was that the Check Entity Group option might solve this. However, it only lists the entities at the end of the Multi-Period Load and performs a consolidation for entities assigned to the Check Entity Group. This does not meet the requirement because the prior period’s closing balance must be calculated before loading the next period’s data.
The solution is to process each period individually and run a consolidation after each period. While I could have used a Groovy loop and allowed the Check Entity Group to manage consolidations, this environment includes multiple integrations. That approach would trigger multiple consolidations per period and significantly increase the pipeline’s runtime.
Instead, the Groovy script runs each integration rule in parallel for one period at a time, with a single consolidation between periods. This approach reduced the overall runtime by approximately 50%.
FCCS Monthly Subvariable Change Automation
This use case is relatively straightforward. Groovy was selected to derive the current close period based on the current calendar period and set the required substitution variables for the automated close process.
Paste-ready rule: ClosePeriodSubvar.groovy. Same EpmRestConfig / callEpm boilerplate as the pipeline example.
Using RTPs (Runtime Prompts), the script performs different actions based on the selected input:
- When the
RUN_TASKRTP is set toUpdate, the script updates the substitution variables based on the current calendar period. - When the
RUN_TASKRTP is set toValidate, the script verifies that the close-period substitution variables are currently set correctly. If any variables are incorrect, the script fails with status code1.
So the Groovy script is scheduled through the Jobs module to update the variables every 15 minutes. Each pipeline that uses the close-period substitution variables also includes an initial task that runs the script with the Validate option. That gates the load on live values instead of trying to fix them mid-pipeline.
EDMCS Metadata Load Pipeline Validation (Pre and Post load)
This process is still being refined, but it is worth noting. We use Groovy to retrieve member information for the selected dimension through multiple endpoints and compare it with the file generated and uploaded by EDMCS.
Paste-ready rules (same helper):
- Pre-load gate:
ValidateEdmMetadataPreload.groovy - Post-load check:
ValidateEdmMetadataPostload.groovy
The script currently evaluates parent-child relationships and identifies the number of metadata deltas. Based on a configurable threshold, it can block the metadata load when the number of deltas exceeds the acceptable limit.
In the future, I plan to expand the validation to include member properties. After the metadata load, a post-load validation confirms that the metadata sent from EDMCS matches what is available in FCCS.
Gotchas
-
Wrong tree / wrong version.
/HyperionPlanning/rest/v3will not run a pipeline./aif/rest/v1(lowercase v) is not/aif/rest/V1. Discover withGETon the tree root before you hardcode. -
PATCH.
Connectionhas nopatch. If the catalog says PATCH, you are not calling it from this helper. -
json(),def, andparseText. Planning type-checks at save. UseJsonOutput.toJson. Do not stash the builder in adefand then call.header().JsonSlurper.parseTextisObject— castas Mapand useget("jobId");job.jobIdwill not save. Nesting a method inside the rule (HttpResponse callEpm(...) { }) will not parse; use the closure. -
Status codes are not interchangeable. Planning jobs and Data Integration jobs both use
-1/0/1, but the JSON shape aroundjobIddiffers. Parse the body you actually got. -
Timeouts. Groovy has a rule timeout.
maxPolls * pollMsin the example is 10 minutes of sleep on top of the POST. If the pipeline is longer, raise the cap or drop the wait and poll from somewhere that can sleep. If you omit the cap, the rule runs until the platform kills it. Also confirm yourBatch Timeout In Minutesare set correctly in Data Exchange.



Comments
Plain text only. No account needed — blank name shows as Anonymous. You can reply once under any top-level comment.
Loading…