A pre-action step inside the tool itself
AI Agent Studio gives an agent its tools as Flow actions, Subflows and Scripts. Make the Veridect call the first step of the action: a script step asks Veridect, and anything other than a greenlight ends the action with the verdict and the record id in the tool’s output — the agent sees why, and so does the ledger.
Fail-closed by construction: proceed starts false and becomes true only on a parsed greenlight — a non-200, a timeout, a thrown request or a malformed body all leave it false, and the work step runs only when it is true. The same call fits a Script tool (string inputs and outputs) or an IntegrationHub REST step. The endpoint and the key live on the REST Message record and its Credential alias, never in script.
// Flow action "Veridect pre-action" — step 1 of any tool an AI agent can call (function execute(inputs, outputs) { outputs.proceed = false; outputs.verdict = 'unavailable'; outputs.record_id = ''; // closed until proven open var agentId = gs.getUserID(); // the identity this action runs as (the agent's service account) — never an input the agent fills in var auth = new GlideRecord('x_veridect_agent_authority'); // your table: one row per agent identity, its authority in words if (!auth.get('agent_user', agentId)) { outputs.verdict = 'unknown_agent'; return; } // no configured authority: never asked, never run var d = null; try { var r = new sn_ws.RESTMessageV2('Veridect', 'pre-action'); // REST Message record: endpoint + credential alias live there, not in code r.setHttpTimeout(60000); r.setRequestHeader('Content-Type', 'application/json'); r.setRequestBody(JSON.stringify({ action: inputs.action, // e.g. "reassign_incident" target: 'servicenow:' + inputs.table, // e.g. "servicenow:incident" parameters: JSON.parse(inputs.parameters_json), // a bad payload throws here — inside the fail-closed try callingAgent: String(auth.getValue('agent_name')), callingAgentScope: String(auth.getValue('scope')), // its configured authority, in words department: String(auth.getValue('department')) })); var resp = r.execute(); // synchronous — the step waits for the verdict if (resp.getStatusCode() === 200) d = JSON.parse(resp.getBody()); } catch (e) { d = null; } // network, timeout, parse: all stay closed if (d && d.verdict) { outputs.verdict = d.verdict; outputs.record_id = d.auditBundleId || ''; outputs.proceed = d.verdict === 'greenlight'; // step 2 (the work) runs only if true } })(inputs, outputs);