Every expression you write inside an Oracle Fusion AI Agent Studio workflow — {{ }} syntax and all — is really just a pointer into one shared runtime object called $context, and that object has exactly six named branches: System, Workflow, User, Trigger, User-defined, and Node variables. Get the branch wrong and the expression doesn't throw a loud error most of the time — it quietly resolves to nothing, and your workflow keeps running on missing data. 🌳
This matters because workflow expressions are how data actually moves through an agentic process — from the raw text a user typed, to the ID of the person who typed it, to the output of a business-object lookup three nodes upstream. A workflow that manages something like employee address changes in HCM touches nearly all six variable types in a single run, and a mismatch between "the value I meant" and "the value the expression actually returns" is one of the fastest ways to ship a workflow that looks correct in testing and behaves wrong in production. 🧩
📑 In This Post
- Scope First: Global, Node, and Trigger
- System Variables
- Workflow Variables
- User Variables
- Trigger Variables
- User-defined Variables
- Node Variables
- Hands-On Lab: Build and Reference Your First Variable
- Enterprise Rollout at Scale
- Common Mistakes (and Why They Happen)
- FAQ
- References & Further Reading
- Summary
🔀 Quick Comparison
| Variable Type | Scope | Sample Expression | Read-only? | Typical Use |
|---|---|---|---|---|
| System | Global | {{$context.$system.$currentDate}} | Yes | Reading date, raw input message, chat history |
| Workflow | Global | {{$context.$workflow.$traceId}} | Yes | Debugging a single run, correlating logs |
| User | Global | {{$context.$user.$name}} | Yes | Auditing who initiated the run |
| Trigger | Trigger | {{$context.$triggers.EMAIL.$input.subject}} | Yes | Reading whatever started this specific run |
| User-defined | Global | {{$context.$variables.verifiedAddress}} | No — you set it | Carrying a value you computed forward |
| Node | Node | {{$context.$nodes.lookupAddr.$output.city}} | Yes | Passing one node's result to the next |
1. Scope First: Global, Node, and Trigger
Before naming the six variable types, it's worth fixing the three scopes they live in, because scope — not the variable's name — is what actually determines where an expression is legal to use. A Global-scoped value, such as the current date or a workflow-level variable, can be referenced from any node anywhere in the workflow. A Node-scoped value only exists relative to a specific node — its input, or the output that node produced — and has no meaning outside that node's position in the flow. A Trigger-scoped value depends entirely on which trigger actually fired, since a single workflow can be started by more than one trigger and each one carries its own distinct set of inputs.
Keep that three-way split in your head as you read the six types below — System, Workflow, and User variables are all Global; User-defined variables are also Global once declared; Trigger variables are Trigger-scoped by definition; and Node variables are Node-scoped by definition. Everything else about how you use them follows from that placement.
2. System Variables
Consider a workflow built to handle employee address updates inside Oracle HCM Cloud — a real pattern Fusion customers have brought to Oracle's own community forums while working through exactly this variable model. The very first thing that workflow needs, before it does anything else, is the raw text of what the employee actually typed: something like "please update my mailing address to 42 Elm Street." That raw text is a System Variable.
System Variables come baked into the runtime itself — the platform populates them automatically and they're strictly read-only, so you never declare or set one, you only read it, using the {{$context.$system...}} syntax. The address-update workflow's very first Code or Logic node typically reads {{$context.$system.$inputMessage}} to capture that free-text request, and may separately read {{$context.$system.$currentDate}} to timestamp when the change was requested for compliance logging.
✅ Worked example: the address-update workflow's first node routes on {{$context.$system.$inputMessage}} to decide whether this is a new address submission or a follow-up question, then passes the same variable downstream to an LLM node that extracts street, city, and postal code as structured fields.
🎯 Use this when: a node anywhere in the workflow needs a platform-level fact — the current timestamp, which trigger type fired, or the exact text the user sent — without caring which upstream node produced it, because System Variables are always available regardless of workflow position.
3. Workflow Variables
Once the address-update workflow is mid-run, the team supporting it needs a way to tell one execution apart from every other execution of the same workflow — especially once an employee reports "my address change didn't go through" and support has to find the exact run that failed. That's what Workflow Variables are for: runtime identifiers about the run itself, rather than about the business data flowing through it.
{{$context.$workflow.$traceId}} is generated fresh for every single run and exists specifically to make debugging and support possible — it's the value you'd log at the start of the workflow and hand to a support ticket. {{$context.$workflow.$name}} is different in kind: it identifies the workflow definition itself, stays stable across runs, and only changes if someone renames the workflow. {{$context.$workflow.$conversationId}} ties the run back to the chat session it came from, which matters when the same conversation triggers multiple workflow runs.
💡 Contrasting example: a developer logs $workflow.$name expecting it to uniquely identify this specific address-change attempt, then can't find the failed run in support logs — because every run of that same workflow shares that value. What they needed was $workflow.$traceId, the per-run identifier, not the per-definition one.
🎯 Use this when: you're instrumenting a workflow for support and observability — log the Trace ID at entry and on every Error Handler path, not the workflow name.
4. User Variables
Back to the address-update workflow: HCM address changes carry compliance and data-privacy requirements, so the workflow has to know, with certainty, whose address is actually being changed and who is asking. That identity comes from a User Variable — a predefined, read-only description of the authenticated person currently interacting with the workflow, available through {{$context.$user...}} syntax, most commonly {{$context.$user.$name}}.
✅ Worked example, continued: before the address-update workflow writes anything to HCM, a condition node checks whether $user.$name matches the employee record being modified, or belongs to an HR administrator acting on someone else's behalf — routing the second case to an approval step instead of an automatic update.
🎯 Use this when: a decision node needs to branch based on who is running the workflow — self-service versus delegated administration is the classic case in HCM-adjacent processes.
5. Trigger Variables
This is the type that generates the most confusion in practice — Fusion customers have posted directly to Oracle's community forums asking whether triggers and variables are effectively the same thing, since the trigger configuration tab and the variables tab look similar at first glance. They aren't the same thing, and the address-update workflow shows exactly why the distinction matters.
Say the same address-update workflow can be started two different ways: an employee types the request into a chat session, or an employee emails HR directly and the workflow is triggered by that inbound email. Each entry point is a separate trigger, and each trigger exposes its own distinct set of input fields — that's what makes Trigger Variables their own scope rather than just another Global variable. A REST trigger exposes whatever fields the calling system sent, referenced as {{$context.$triggers.REST.$input.<field>}}. An email trigger exposes a fixed, richer set — subject, sender address, body content, headers, and attachments — for example {{$context.$triggers.EMAIL.$input.fromAddress}} or {{$context.$triggers.EMAIL.$input.attachments}}.
💡 Key warning: a workflow author builds the email-initiated path, tests it, then adds a chat-initiated path months later and reuses the same expression referencing $triggers.EMAIL.$input.fromAddress for both. On the chat path there was never an email trigger, so that expression comes back blank with no error raised anywhere — and any downstream logic keyed on sender address quietly stops working for chat-originated runs.
🎯 Use this when: a workflow can be started by more than one trigger — branch early on $system.$triggerType and only reference each trigger's own input fields inside the branch that actually corresponds to it.
6. User-defined Variables
Everything so far has been provided by the platform. User-defined Variables are the one type you create yourself, shared across every node in the workflow once declared, and referenced with {{$context.$variables.<name>}}. In the address-update workflow, after an LLM node extracts the raw address text into structured fields and a validation step confirms the postal code is real, the author needs somewhere to hold that confirmed result until the final HCM write step runs several nodes later — that's a user-defined variable, commonly named something like verifiedAddress.
Creating one is a two-part process. First, on the workflow's Variables tab, you add the variable by name, choose its type (string, for instance), and set its scope to either User Question or Conversation. Second, because declaring a variable doesn't give it a value, you add a Logic > Set Variables node at the point in the flow where the value becomes known, select the variable by name from the Variables section, and assign it either a static value or one derived dynamically from anywhere else in the runtime context — including another node's output.
✅ Worked example, continued: the Set Variables node assigns verifiedAddress from the validation node's output, and the final HCM write node references {{$context.$variables.verifiedAddress}} instead of reaching four nodes backward to re-derive the same value.
🎯 Use this when: a value needs to survive past the node that produced it and be reused two, three, or more steps later — carrying it forward as a user-defined variable is more resilient than assuming every downstream node can still see the original producing node's output.
7. Node Variables
The sixth type doesn't appear by that exact name in every summary of workflow variables, but it's the one referenced constantly in practice, because it's how any node gets data from any other node. Node Variables cover four related things: a node's own input, a node's output, the error details available when that node fails, and the node's execution status — and all four are scoped strictly to that one node's position in the flow, which is why moving a node changes what its Node Variables mean.
In the address-update workflow, the node that looks up the employee's current address in HCM produces an output object. The next node can pull the whole thing with {{$context.$nodes.lookupAddr.$output}} — useful when handing the entire object to an LLM node for reasoning — or pull one field with {{$context.$nodes.lookupAddr.$output.city}}, which is the right choice for writing a condition, assigning a user-defined variable, or populating a short field like an email subject line.
If that same HCM lookup node fails — say the employee record can't be found — an attached Error Handler can read {{$context.$nodes.lookupAddr.$error.$info}} for a human-readable explanation and {{$context.$nodes.lookupAddr.$error.$code}} for a machine-readable code to branch on — send the employee a friendly "we couldn't find that record" message for one error code, and page an HR admin for another.
💡 Key warning: pointing at a specific output field the node never actually returned — because the upstream node's shape changed, or it failed silently — just comes back blank instead of throwing anything you'd notice. A condition node built on $nodes.lookupAddr.$output.city will happily evaluate against an empty value and route the workflow down whatever branch handles "no data," with nothing in the run log calling that out as unusual.
🎯 Use this when: you're deciding between passing an entire node's input/output object versus a single field — default to specific fields for conditions, variable assignments, and short text, and reserve entire objects for cases where an LLM node genuinely needs the full context to reason well.
8. Hands-On Lab: Build and Reference Your First Variable
This lab is scoped to a single, disposable test workflow — no real HCM data involved — so you can see a user-defined variable get created, assigned, and read back before you touch anything production-facing.
testGreeting, set its type to string, and set its scope to Conversation.testGreeting from the Name field, and set its value to the expression {{$context.$system.$inputMessage}} — a dynamic value derived from runtime context.{{$context.$variables.testGreeting}}.💡 Most common first-timer mistake: declaring the variable on the Variables tab and assuming that alone gives it a value. Declaring only creates the named slot — nothing populates it until a Set Variables node explicitly assigns something, which is easy to forget since the declaration step feels like "creating" the variable in the everyday sense of the word.
That's the whole toy version. The production pattern is identical — declare, assign with Set Variables, reference downstream — the only difference is that in the address-update workflow from earlier sections, the assigned value comes from a validation node's output instead of a raw echo of the input message.
9. Enterprise Rollout at Scale
Naming conventions. Standardize node codes and user-defined variable names across every workflow a team builds — lookupAddr, verifiedAddress, and similar descriptive, consistent names make expressions like $nodes.lookupAddr.$output.city readable to the next developer, instead of forcing them to trace node IDs by hand every time they review an expression.
Field-specific over whole-object, by default. Set an explicit team standard that expressions reference specific output fields rather than entire node objects unless there's a documented reason — usually, passing full context to an LLM reasoning node. This keeps prompts smaller, keeps conditions predictable, and makes it obvious in review when someone is deliberately choosing the wider option.
Versioning discipline around node outputs. Because Node Variables are positional and shape-dependent, changing what a node returns — adding, removing, or renaming an output field — silently breaks every downstream expression that referenced the old shape, without any build failure to catch it. Treat a node's output schema the same way you'd treat a public API contract: changes get reviewed specifically for what downstream expressions they might silently null out.
Enforcement without a CI pipeline. Workflow expressions aren't compiled, so there's no automatic build failure when a reference breaks. The practical substitute is disciplined test-run review before promotion: run the workflow end to end after any node-output change, and specifically check for values that silently resolved to empty where a real value was expected — since that's exactly the failure mode expressions don't announce on their own.
Metrics. Track how often production runs produce empty or null values on expressions that should have real data — a rising trend is usually the earliest signal that an upstream node's output shape changed and nobody updated the expressions depending on it.
10. Common Mistakes (and Why They Happen)
- Treating Trigger Variables as just another Global variable. This happens because the trigger configuration screen and the variables screen look similar at a glance, and it's the exact confusion Fusion customers have raised directly with Oracle's own community — but Trigger Variables are scoped to whichever trigger actually fired, not available workflow-wide the way System or User variables are.
- Defaulting to whole node objects instead of specific fields. It feels safer to pass everything forward "just in case," but it inflates LLM prompts unnecessarily and makes conditions harder to reason about — the guidance is explicit that specific fields are the right choice for conditions, variable assignments, and short text.
- Assuming a missing field throws an error. Pointing an expression at a field the node never returns just comes back blank, not a visible failure — so a typo in a field name, or an upstream schema change, produces a workflow that runs to completion while quietly doing the wrong thing.
- Confusing Workflow Name with Trace ID. Because both live under
$workflow, it's easy to log the wrong one — Name identifies the workflow definition and stays stable across runs, while Trace ID is unique per run and is the one debugging actually needs. - Declaring a user-defined variable and assuming that alone gives it a value. Declaration on the Variables tab only reserves the name and type; nothing populates it until an explicit Set Variables node runs, which is a step first-time builders frequently skip.
❓ FAQ
What's the real difference between System Variables and User-defined Variables?
System Variables are predefined and read-only, provided by the platform itself — you can only read them. User-defined Variables are ones you declare and populate yourself using a Set Variables node, and they're the only type you can actually write to.
How do I reference just one field from a node's output instead of the whole thing?
Use the specific-field form of the expression, such as {{$context.$nodes.<nodeCode>.$output.<field>}}, rather than {{$context.$nodes.<nodeCode>.$output}} — the latter returns the entire output object.
What happens if I reference a node output field that doesn't exist?
It just comes back blank rather than throwing a visible error. This is exactly why field-name typos and upstream schema changes are hard to catch without deliberately testing for unexpectedly empty values.
Are Trigger Variables the same across REST and Email triggers?
No. A REST trigger exposes whatever fields the calling request sent, referenced under $triggers.REST.$input.<field>. An Email trigger exposes a fixed set — subject, sender address, body content, headers, and attachments — under $triggers.EMAIL.$input.*. Each trigger type has its own distinct input shape.
How does a user-defined variable actually get its initial value?
Declaring it on the Variables tab only creates the name, type, and scope — it doesn't set a value. You assign a value separately with a Logic > Set Variables node, using either a static value or one derived dynamically from the runtime context, such as another node's output.
🔗 References & Further Reading
Primary / official sources relied on for facts in this post:
- Oracle Fusion Cloud documentation — "Variables Specific to Workflows" (docs.oracle.com, Fusion AI 26C)
- Oracle Fusion Cloud documentation — "Expressions" (docs.oracle.com, Fusion AI 26C)
Oracle, Oracle Fusion Cloud Applications, AI Agent Studio, and related marks are trademarks of Oracle Corporation and/or its affiliates. All product names referenced belong to their respective owners. This post synthesizes and explains publicly available information in original wording; it does not reproduce source text verbatim and is not an official Oracle publication.
📝 Summary
- Every workflow expression resolves through one $context object with three scopes — Global, Node, and Trigger — and six named variable types living inside them.
- System Variables are read-only platform facts like current date and raw input message, available anywhere in the workflow.
- Workflow Variables identify the run itself — Trace ID for a single execution, Workflow Name for the definition, Conversation ID for the session.
- User Variables describe who's currently running the workflow, useful for compliance and self-service-versus-delegated branching.
- Trigger Variables are scoped to whichever trigger actually fired, and REST versus Email triggers expose entirely different input shapes.
- User-defined Variables are the one type you create and populate yourself, via declaration plus a Set Variables node.
- Node Variables — input, output, error details, and status — are scoped to a single node's position, and missing fields resolve silently to null rather than erroring.
- At scale, the real governance work is naming conventions, defaulting to specific fields over whole objects, and treating node output shapes like versioned contracts.
Comments
Post a Comment