Flow is getting failed to give output when executed via script .the flow has some for loop to check
- Mark as New
- Bookmark
- Subscribe
- Mute
- Subscribe to RSS Feed
- Permalink
- Report Inappropriate Content
3 weeks ago
Hi all,
I’m trying to call a subflow from a Script Include (triggered via GlideAjax from a Catalog Client Script) and return the output to populate a select box dynamically on a catalog item whle submiting.
Current flow:
- User selects a subnet on the Service Portal catalog item
- onChange Client Script calls Script Include via GlideAjax
- Script Include calls subflow using:
sn_fd.FlowAPI.getRunner()
.subflow('global.subnet_ranges')
.inForeground()
.withInputs(inputs)
.timeout(300000)
.run();- Then tries to get outputs:
var outputs = result.getOutputs();
var subnet_ranges = outputs.subnet_ranges;Problem:
I get the error:
The current execution is in the waiting stateIf I increase timeout, I instead hit:
An execution timed out with timeout of 30000 MILLISECONDSObservation:
The subflow includes steps like:
- Launch Job Template (Ansible)
- Possibly waits for completion / external processing and pass outpus
So it does not complete immediately.
What I’m trying to achieve:
- Fetch subnet ranges dynamically based on selected subnet field on portal of a catalog item
- get the outputs from subflow and add them as option and Populate a dropdown field (
subnet_ranges) on the catalog item before submission
Questions:
- Is it expected thatinForegeround() fails if the subflow contains wait/async steps?
- Is there any supported way to synchronously return outputs from such a subflow?
- If not, what is the recommended pattern for this use case (catalog client script → dynamic dropdown)?
- Is using inBackground()+ storing results in a custom table + polling via GlideAjax the correct approach?
Would appreciate guidance or best practices for this pattern.
Thanks!
- Mark as New
- Bookmark
- Subscribe
- Mute
- Subscribe to RSS Feed
- Permalink
- Report Inappropriate Content
3 weeks ago
**TL;DR:** `inForeground()` cannot execute subflows that contain async steps (like Ansible jobs). This is by design — not a bug. The simplest fix: cache the results with a scheduled job and skip the real-time complexity entirely.
---
## Why Your Current Code Fails
`inForeground()` runs the subflow on the calling server thread — that thread blocks, waiting for completion. Your Ansible step requires the flow engine to PAUSE (send request to Ansible, wait for callback). A foreground execution can't release, pause, and resume. The engine detects this and throws the "waiting state" error.
There's also a second timeout you may not have noticed — GlideAjax has its own client-side timeout (typically 30-60 seconds). Even if foreground execution could wait, the AJAX call would die first. Two independent clocks, neither visible to the other.
**To your questions:**
1. Yes, you can get output from FlowAPI in a Script Include — but the subflow must be fully synchronous for `inForeground()` to work.
2. Yes, FlowAPI via GlideAjax has TWO timeout constraints (client AJAX + server FlowAPI) that don't exist in native flow-to-subflow calls.
3. Yes, use `inBackground()` for subflows with async steps.
4. Yes, the custom table approach is viable — and it's the pattern I'd recommend. Full implementation below.
**Quick diagnostic first:** Run your subflow manually in Flow Designer with test inputs. If it succeeds there but fails via FlowAPI, the issue is the execution mode — confirming this diagnosis. If it also fails in Flow Designer, the problem may be simpler (scope, permissions, config).
---
## The Fix — Cache Pattern
Before wiring up a polling mechanism, ask: **how often do your subnet ranges actually change?**
If the answer is "not every time someone opens the catalog item" — and for most network infrastructure data, it doesn't — you can avoid all async complexity by pre-caching the results.
**The pattern:** A scheduled job runs your Ansible subflow on an interval. The subflow writes results to a custom table. Your dropdown reads from that table via GlideAjax — fast, synchronous, no polling, no timeout complexity. The dropdown loads instantly because the data is already there.
### Step 1: Create the cache table
Create `u_subnet_range_cache` with these fields:
| Field | Type | Purpose |
|-------|------|---------|
| u_range_name | String | Subnet range identifier |
| u_range_value | String | Value for your dropdown |
| u_last_refreshed | Date/Time | When this row was last updated |
| u_ttl_expires | Date/Time | When this row is considered stale |
| u_source_job | String | Execution ID of the flow that produced this row |
### Step 2: Modify your subflow
Add a **Run Script** step as the LAST action in your existing subflow. This writes results to the cache table:
```javascript
// Add this as the final step in your subflow (Flow Designer > Run Script action)
// Replace <your range name output> and <your range value output> with your
// actual subflow output variables.
var gr = new GlideRecord('u_subnet_range_cache');
gr.initialize();
gr.setValue('u_range_name', <your range name output>);
gr.setValue('u_range_value', <your range value output>);
gr.setValue('u_last_refreshed', new GlideDateTime());
var ttlExpiry = new GlideDateTime();
ttlExpiry.addSeconds(24 * 3600); // 24 hours — match your scheduled job interval
gr.setValue('u_ttl_expires', ttlExpiry);
gr.setValue('u_source_job', fd_data.flow_execution_id);
gr.insert();
```
**Without this step, the cache table stays empty and your dropdown will never populate.**
### Step 3: Create the scheduled job
Scheduled Script Execution — triggers the subflow and cleans stale rows:
```javascript
(function() {
var SUBFLOW_NAME = 'global.subnet_ranges'; // Replace with YOUR subflow's internal name
var CACHE_TABLE = 'u_subnet_range_cache';
var TTL_HOURS = 24;
if (TTL_HOURS <= 0) {
gs.error('Subnet Cache Refresh: TTL_HOURS must be > 0. Aborting.');
return;
}
try {
sn_fd.FlowAPI.getRunner()
.subflow(SUBFLOW_NAME)
.inBackground()
.run();
// Cleanup stale rows
var staleDate = new GlideDateTime();
staleDate.addSeconds(-(TTL_HOURS * 3600));
var gr = new GlideRecord(CACHE_TABLE);
gr.addQuery('u_ttl_expires', '<', staleDate);
gr.query();
var deleteCount = gr.getRowCount();
// Safety: don't wipe the entire table on a config error
var totalGr = new GlideRecord(CACHE_TABLE);
totalGr.query();
var totalCount = totalGr.getRowCount();
if (totalCount > 0 && deleteCount >= totalCount) {
gs.warn('Subnet Cache Refresh: Cleanup would delete ALL ' + totalCount +
' rows. Skipping — check TTL_HOURS.');
} else {
gr = new GlideRecord(CACHE_TABLE);
gr.addQuery('u_ttl_expires', '<', staleDate);
gr.deleteMultiple();
}
gs.info('Subnet Cache Refresh: Subflow triggered, stale rows cleaned.');
} catch (e) {
gs.error('Subnet Cache Refresh failed: ' + e.message);
}
})();
```
### Step 4: Script Include (client-callable)
```javascript
var SubnetRangeCacheReader = Class.create();
SubnetRangeCacheReader.prototype = Object.extendsObject(AbstractAjaxProcessor, {
_ALLOWED_METHODS: ['getSubnetRanges'],
_isCallerAuthorized: function() {
if (!gs.hasRole('itil')) {
gs.warn('SubnetRangeCacheReader: Unauthorized access by ' + gs.getUserName());
return false;
}
return true;
},
_isAllowedMethod: function(methodName) {
return this._ALLOWED_METHODS.indexOf(methodName) !== -1;
},
getSubnetRanges: function() {
if (!this._isAllowedMethod(this.getParameter('sysparm_name'))) {
return JSON.stringify({ error: 'Method not allowed' });
}
if (!this._isCallerAuthorized()) {
return JSON.stringify({ error: 'Unauthorized' });
}
var ranges = [];
var gr = new GlideRecord('u_subnet_range_cache');
gr.addQuery('u_ttl_expires', '>', new GlideDateTime());
gr.orderBy('u_range_name');
gr.query();
while (gr.next()) {
ranges.push({
name: gr.getValue('u_range_name'),
value: gr.getValue('u_range_value')
});
}
return JSON.stringify({ ranges: ranges });
},
type: 'SubnetRangeCacheReader'
});
```
**Note on security:** Every client-callable Script Include is accessible to any authenticated user. The `_ALLOWED_METHODS` whitelist + role check above is not optional — without it, anyone logged in can query your infrastructure data from their browser console.
### Step 5: Catalog Client Script (onLoad)
```javascript
function onLoad() {
var ga = new GlideAjax('SubnetRangeCacheReader');
ga.addParam('sysparm_name', 'getSubnetRanges');
ga.getXMLAnswer(function(response) {
try {
var data = JSON.parse(response);
if (data.error) {
g_form.addErrorMessage('Unable to load subnet ranges: ' + data.error);
return;
}
var choices = data.ranges || [];
if (choices.length === 0) {
g_form.addInfoMessage('No subnet ranges available. ' +
'Data may still be loading — try again in a few minutes.');
return;
}
var fieldName = 'your_dropdown_variable'; // REPLACE THIS
g_form.clearOptions(fieldName);
g_form.addOption(fieldName, '', '-- Select a Subnet Range --');
for (var i = 0; i < choices.length; i++) {
g_form.addOption(fieldName, choices[i].value, choices[i].name);
}
} catch (e) {
g_form.addErrorMessage('Error parsing subnet range data. Contact your administrator.');
}
});
}
```
---
**That's it.** No polling, no timeout alignment across three systems, no phantom executions. The dropdown loads in milliseconds because the data is already cached. The scheduled job handles the Ansible complexity in the background on a schedule you control.
If you genuinely need real-time data (the subnet ranges change between every catalog item open), you'd need a polling pattern with `inBackground()` + `setInterval()` on the client side — but in most infrastructure scenarios, caching at hourly or daily intervals is the right answer.
*All code is provided as architectural patterns — test on your instance before deploying.*
