sndev.io / docs examples
← Example Applications

Example 01 / Event Queue

An event in.
A delivery you can trace.

Accept an order, queue it, and route its delivery by a stored threshold. A small integration with real application concerns: input validation, duplicate handling, persisted state and a requirement that changes.

Download the manifest ↓

Contract

One active routing rule. A threshold stored as data.

Event

The incoming order, its processing state and linked delivery.

Delivery

The amount, currency and selected route.

Seven steps. One reviewable application.

This manifest adapts our behavioral reference app to a fresh scope and current scoped field names. The published file passes local preflight and planning; the live measurements below belong to the earlier retained reference app. The contract row is seeded separately because it is runtime data.

StepOperationPurpose
contracttable/create_tablex_eq_demo_contract — event type, threshold, active
eventtable/create_tablex_eq_demo_event — event_id, payload, state, route
deliverytable/create_tablex_eq_demo_delivery — order_id, amount, currency, route
apiscripted-rest/create_apiScripted REST service: event_queue
submitscripted-rest/create_resourcePOST /events — validates and queues
processscripted-rest/create_resourcePOST /process/{event_id} — one delivery, threshold routing
readscripted-rest/create_resourceGET /events/{event_id} — persisted state
Inspect the complete JSON manifest
manifest.sn.json
{
  "schemaVersion": 1,
  "scope": {
    "prefix": "x_eq_demo",
    "name": "Event Queue example",
    "create": true
  },
  "updateSet": "Event Queue example",
  "steps": [
    {
      "id": "contract",
      "skill": "table",
      "op": "create_table",
      "inputs": {
        "name": "x_eq_demo_contract",
        "label": "Event Queue contract",
        "sysScope": "$scope",
        "schema": [
          {
            "column": "type",
            "label": "type",
            "internalType": "string"
          },
          {
            "column": "threshold",
            "label": "threshold",
            "internalType": "integer"
          },
          {
            "column": "active",
            "label": "active",
            "internalType": "boolean"
          }
        ]
      }
    },
    {
      "id": "event",
      "skill": "table",
      "op": "create_table",
      "inputs": {
        "name": "x_eq_demo_event",
        "label": "Event Queue event",
        "sysScope": "$scope",
        "schema": [
          {
            "column": "event_id",
            "label": "event_id",
            "internalType": "string"
          },
          {
            "column": "payload",
            "label": "payload",
            "internalType": "string",
            "maxLength": 32768
          },
          {
            "column": "state",
            "label": "state",
            "internalType": "string"
          },
          {
            "column": "route",
            "label": "route",
            "internalType": "string"
          },
          {
            "column": "contract_ref",
            "label": "contract_ref",
            "internalType": "reference",
            "reference": "x_eq_demo_contract"
          },
          {
            "column": "delivery",
            "label": "delivery",
            "internalType": "string",
            "maxLength": 32
          }
        ]
      }
    },
    {
      "id": "delivery",
      "skill": "table",
      "op": "create_table",
      "inputs": {
        "name": "x_eq_demo_delivery",
        "label": "Event Queue delivery",
        "sysScope": "$scope",
        "schema": [
          {
            "column": "event",
            "label": "event",
            "internalType": "reference",
            "reference": "x_eq_demo_event"
          },
          {
            "column": "order_id",
            "label": "order_id",
            "internalType": "string"
          },
          {
            "column": "amount",
            "label": "amount",
            "internalType": "integer"
          },
          {
            "column": "currency",
            "label": "currency",
            "internalType": "string"
          },
          {
            "column": "route",
            "label": "route",
            "internalType": "string"
          }
        ]
      }
    },
    {
      "id": "api",
      "skill": "scripted-rest",
      "op": "create_api",
      "inputs": {
        "name": "Event Queue",
        "serviceId": "event_queue",
        "sysScope": "$scope",
        "consumes": "application/json",
        "produces": "application/json"
      }
    },
    {
      "id": "submit",
      "skill": "scripted-rest",
      "op": "create_resource",
      "inputs": {
        "name": "submit",
        "method": "POST",
        "path": "/events",
        "script": "(function (request, response) {\n  function reply(status, body) { response.setStatus(status); response.setBody(body); }\n  function fail(status, text) { reply(status, { error: { message: text } }); }\n  function find(table, field, val) {\n    var gr = new GlideRecord(table); gr.addQuery(field, val); gr.query(); return gr;\n  }\n  function contract() {\n    var c = new GlideRecord('x_eq_demo_contract');\n    c.addQuery('type', 'order.created'); c.addQuery('active', true); c.query();\n    if (!c.next()) throw new Error('Missing active contract');\n    var result = { id: c.getUniqueValue(), threshold: Number(c.getValue('threshold')) };\n    if (!isFinite(result.threshold) || c.next()) throw new Error('Invalid or ambiguous contract');\n    return result;\n  }\n  function canonical(v) {\n    if (v === null || typeof v !== 'object') return JSON.stringify(v);\n    var keys = Object.keys(v).sort(), parts = [];\n    for (var i = 0; i < keys.length; i++) parts.push(JSON.stringify(keys[i]) + ':' + canonical(v[keys[i]]));\n    return '{' + parts.join(',') + '}';\n  }\n  function keysAllowed(obj, allowed) {\n    var keys = Object.keys(obj);\n    for (var i = 0; i < keys.length; i++) if (allowed.indexOf(keys[i]) < 0) return false;\n    return true;\n  }\n  try {\n    var mode = 'submit', eventId, gr, payload, c;\n    if (mode === 'submit') {\n      var body;\n      try { body = JSON.parse(String(request.body.dataString)); }\n      catch (parseError) { fail(400, 'Malformed JSON'); return; }\n      if (!body || typeof body !== 'object' || Array.isArray(body)) { fail(400, 'Object required'); return; }\n      payload = body.payload; eventId = body.event_id;\n      var identifier = /^[A-Za-z0-9_-]{1,64}$/;\n      if (!keysAllowed(body, ['event_id', 'event_type', 'payload']) ||\n          typeof eventId !== 'string' || !identifier.test(eventId) || body.event_type !== 'order.created' ||\n          !payload || typeof payload !== 'object' || Array.isArray(payload) ||\n          !keysAllowed(payload, ['order_id', 'amount', 'currency', 'note']) ||\n          typeof payload.order_id !== 'string' || !identifier.test(payload.order_id) ||\n          typeof payload.amount !== 'number' || !isFinite(payload.amount) ||\n          Math.floor(payload.amount) !== payload.amount || payload.amount < 0 || payload.amount > 1000000 ||\n          (payload.currency !== 'USD' && payload.currency !== 'EUR') ||\n          (Object.prototype.hasOwnProperty.call(payload, 'note') && typeof payload.note !== 'string')) {\n        fail(422, 'Invalid event'); return;\n      }\n      if (payload.note !== undefined) {\n        var bytes;\n        try { bytes = encodeURIComponent(payload.note).replace(/%[0-9A-F]{2}/gi, 'x').length; }\n        catch (unicodeError) { fail(422, 'Invalid Unicode'); return; }\n        if (bytes > 4096) { fail(413, 'Note exceeds 4096 UTF-8 bytes'); return; }\n      }\n      gr = find('x_eq_demo_event', 'event_id', eventId);\n      if (gr.next()) {\n        if (canonical(JSON.parse(gr.getValue('payload'))) !== canonical(payload)) { fail(409, 'Conflicting duplicate'); return; }\n        reply(200, { event_id: eventId, record_id: gr.getUniqueValue(), state: gr.getValue('state'), duplicate: true }); return;\n      }\n      c = contract();\n      gr.initialize(); gr.setValue('event_id', eventId); gr.setValue('payload', JSON.stringify(payload));\n      gr.setValue('state', 'queued'); gr.setValue('contract_ref', c.id);\n      var eventRecord = gr.insert();\n      if (!eventRecord) throw new Error('Event insert failed');\n      reply(202, { event_id: eventId, record_id: String(eventRecord), state: 'queued', duplicate: false }); return;\n    }\n    eventId = request.pathParams.event_id;\n    gr = find('x_eq_demo_event', 'event_id', eventId);\n    if (!gr.next()) { fail(404, 'Unknown event'); return; }\n    payload = JSON.parse(gr.getValue('payload'));\n    if (mode === 'process' && gr.getValue('state') !== 'processed') {\n      c = contract();\n      var delivery = new GlideRecord('x_eq_demo_delivery'); delivery.initialize();\n      delivery.setValue('event', gr.getUniqueValue()); delivery.setValue('order_id', payload.order_id);\n      delivery.setValue('amount', payload.amount); delivery.setValue('currency', payload.currency);\n      var route = payload.amount < c.threshold ? 'standard' : 'priority'; delivery.setValue('route', route);\n      var deliveryId = delivery.insert();\n      if (!deliveryId) throw new Error('Delivery insert failed');\n      gr.setValue('state', 'processed'); gr.setValue('route', route); gr.setValue('delivery', deliveryId);\n      if (!gr.update()) throw new Error('Event update failed');\n    }\n    var result = { event_id: eventId, state: gr.getValue('state'),\n      route: gr.getValue('route') || null, delivery_id: gr.getValue('delivery') || null };\n    if (mode === 'read') {\n      result.record_id = gr.getUniqueValue(); result.event_type = 'order.created'; result.payload = payload;\n    }\n    reply(200, result);\n  } catch (error) { fail(500, String(error.message || error)); }\n})(request, response);\n",
        "webServiceDefinition": "$api",
        "sysScope": "$scope",
        "requiresAuthentication": true,
        "requiresAclAuthorization": false
      }
    },
    {
      "id": "process",
      "skill": "scripted-rest",
      "op": "create_resource",
      "inputs": {
        "name": "process",
        "method": "POST",
        "path": "/process/{event_id}",
        "script": "(function (request, response) {\n  function reply(status, body) { response.setStatus(status); response.setBody(body); }\n  function fail(status, text) { reply(status, { error: { message: text } }); }\n  function find(table, field, val) {\n    var gr = new GlideRecord(table); gr.addQuery(field, val); gr.query(); return gr;\n  }\n  function contract() {\n    var c = new GlideRecord('x_eq_demo_contract');\n    c.addQuery('type', 'order.created'); c.addQuery('active', true); c.query();\n    if (!c.next()) throw new Error('Missing active contract');\n    var result = { id: c.getUniqueValue(), threshold: Number(c.getValue('threshold')) };\n    if (!isFinite(result.threshold) || c.next()) throw new Error('Invalid or ambiguous contract');\n    return result;\n  }\n  function canonical(v) {\n    if (v === null || typeof v !== 'object') return JSON.stringify(v);\n    var keys = Object.keys(v).sort(), parts = [];\n    for (var i = 0; i < keys.length; i++) parts.push(JSON.stringify(keys[i]) + ':' + canonical(v[keys[i]]));\n    return '{' + parts.join(',') + '}';\n  }\n  function keysAllowed(obj, allowed) {\n    var keys = Object.keys(obj);\n    for (var i = 0; i < keys.length; i++) if (allowed.indexOf(keys[i]) < 0) return false;\n    return true;\n  }\n  try {\n    var mode = 'process', eventId, gr, payload, c;\n    if (mode === 'submit') {\n      var body;\n      try { body = JSON.parse(String(request.body.dataString)); }\n      catch (parseError) { fail(400, 'Malformed JSON'); return; }\n      if (!body || typeof body !== 'object' || Array.isArray(body)) { fail(400, 'Object required'); return; }\n      payload = body.payload; eventId = body.event_id;\n      var identifier = /^[A-Za-z0-9_-]{1,64}$/;\n      if (!keysAllowed(body, ['event_id', 'event_type', 'payload']) ||\n          typeof eventId !== 'string' || !identifier.test(eventId) || body.event_type !== 'order.created' ||\n          !payload || typeof payload !== 'object' || Array.isArray(payload) ||\n          !keysAllowed(payload, ['order_id', 'amount', 'currency', 'note']) ||\n          typeof payload.order_id !== 'string' || !identifier.test(payload.order_id) ||\n          typeof payload.amount !== 'number' || !isFinite(payload.amount) ||\n          Math.floor(payload.amount) !== payload.amount || payload.amount < 0 || payload.amount > 1000000 ||\n          (payload.currency !== 'USD' && payload.currency !== 'EUR') ||\n          (Object.prototype.hasOwnProperty.call(payload, 'note') && typeof payload.note !== 'string')) {\n        fail(422, 'Invalid event'); return;\n      }\n      if (payload.note !== undefined) {\n        var bytes;\n        try { bytes = encodeURIComponent(payload.note).replace(/%[0-9A-F]{2}/gi, 'x').length; }\n        catch (unicodeError) { fail(422, 'Invalid Unicode'); return; }\n        if (bytes > 4096) { fail(413, 'Note exceeds 4096 UTF-8 bytes'); return; }\n      }\n      gr = find('x_eq_demo_event', 'event_id', eventId);\n      if (gr.next()) {\n        if (canonical(JSON.parse(gr.getValue('payload'))) !== canonical(payload)) { fail(409, 'Conflicting duplicate'); return; }\n        reply(200, { event_id: eventId, record_id: gr.getUniqueValue(), state: gr.getValue('state'), duplicate: true }); return;\n      }\n      c = contract();\n      gr.initialize(); gr.setValue('event_id', eventId); gr.setValue('payload', JSON.stringify(payload));\n      gr.setValue('state', 'queued'); gr.setValue('contract_ref', c.id);\n      var eventRecord = gr.insert();\n      if (!eventRecord) throw new Error('Event insert failed');\n      reply(202, { event_id: eventId, record_id: String(eventRecord), state: 'queued', duplicate: false }); return;\n    }\n    eventId = request.pathParams.event_id;\n    gr = find('x_eq_demo_event', 'event_id', eventId);\n    if (!gr.next()) { fail(404, 'Unknown event'); return; }\n    payload = JSON.parse(gr.getValue('payload'));\n    if (mode === 'process' && gr.getValue('state') !== 'processed') {\n      c = contract();\n      var delivery = new GlideRecord('x_eq_demo_delivery'); delivery.initialize();\n      delivery.setValue('event', gr.getUniqueValue()); delivery.setValue('order_id', payload.order_id);\n      delivery.setValue('amount', payload.amount); delivery.setValue('currency', payload.currency);\n      var route = payload.amount < c.threshold ? 'standard' : 'priority'; delivery.setValue('route', route);\n      var deliveryId = delivery.insert();\n      if (!deliveryId) throw new Error('Delivery insert failed');\n      gr.setValue('state', 'processed'); gr.setValue('route', route); gr.setValue('delivery', deliveryId);\n      if (!gr.update()) throw new Error('Event update failed');\n    }\n    var result = { event_id: eventId, state: gr.getValue('state'),\n      route: gr.getValue('route') || null, delivery_id: gr.getValue('delivery') || null };\n    if (mode === 'read') {\n      result.record_id = gr.getUniqueValue(); result.event_type = 'order.created'; result.payload = payload;\n    }\n    reply(200, result);\n  } catch (error) { fail(500, String(error.message || error)); }\n})(request, response);\n",
        "webServiceDefinition": "$api",
        "sysScope": "$scope",
        "requiresAuthentication": true,
        "requiresAclAuthorization": false
      }
    },
    {
      "id": "read",
      "skill": "scripted-rest",
      "op": "create_resource",
      "inputs": {
        "name": "read",
        "method": "GET",
        "path": "/events/{event_id}",
        "script": "(function (request, response) {\n  function reply(status, body) { response.setStatus(status); response.setBody(body); }\n  function fail(status, text) { reply(status, { error: { message: text } }); }\n  function find(table, field, val) {\n    var gr = new GlideRecord(table); gr.addQuery(field, val); gr.query(); return gr;\n  }\n  function contract() {\n    var c = new GlideRecord('x_eq_demo_contract');\n    c.addQuery('type', 'order.created'); c.addQuery('active', true); c.query();\n    if (!c.next()) throw new Error('Missing active contract');\n    var result = { id: c.getUniqueValue(), threshold: Number(c.getValue('threshold')) };\n    if (!isFinite(result.threshold) || c.next()) throw new Error('Invalid or ambiguous contract');\n    return result;\n  }\n  function canonical(v) {\n    if (v === null || typeof v !== 'object') return JSON.stringify(v);\n    var keys = Object.keys(v).sort(), parts = [];\n    for (var i = 0; i < keys.length; i++) parts.push(JSON.stringify(keys[i]) + ':' + canonical(v[keys[i]]));\n    return '{' + parts.join(',') + '}';\n  }\n  function keysAllowed(obj, allowed) {\n    var keys = Object.keys(obj);\n    for (var i = 0; i < keys.length; i++) if (allowed.indexOf(keys[i]) < 0) return false;\n    return true;\n  }\n  try {\n    var mode = 'read', eventId, gr, payload, c;\n    if (mode === 'submit') {\n      var body;\n      try { body = JSON.parse(String(request.body.dataString)); }\n      catch (parseError) { fail(400, 'Malformed JSON'); return; }\n      if (!body || typeof body !== 'object' || Array.isArray(body)) { fail(400, 'Object required'); return; }\n      payload = body.payload; eventId = body.event_id;\n      var identifier = /^[A-Za-z0-9_-]{1,64}$/;\n      if (!keysAllowed(body, ['event_id', 'event_type', 'payload']) ||\n          typeof eventId !== 'string' || !identifier.test(eventId) || body.event_type !== 'order.created' ||\n          !payload || typeof payload !== 'object' || Array.isArray(payload) ||\n          !keysAllowed(payload, ['order_id', 'amount', 'currency', 'note']) ||\n          typeof payload.order_id !== 'string' || !identifier.test(payload.order_id) ||\n          typeof payload.amount !== 'number' || !isFinite(payload.amount) ||\n          Math.floor(payload.amount) !== payload.amount || payload.amount < 0 || payload.amount > 1000000 ||\n          (payload.currency !== 'USD' && payload.currency !== 'EUR') ||\n          (Object.prototype.hasOwnProperty.call(payload, 'note') && typeof payload.note !== 'string')) {\n        fail(422, 'Invalid event'); return;\n      }\n      if (payload.note !== undefined) {\n        var bytes;\n        try { bytes = encodeURIComponent(payload.note).replace(/%[0-9A-F]{2}/gi, 'x').length; }\n        catch (unicodeError) { fail(422, 'Invalid Unicode'); return; }\n        if (bytes > 4096) { fail(413, 'Note exceeds 4096 UTF-8 bytes'); return; }\n      }\n      gr = find('x_eq_demo_event', 'event_id', eventId);\n      if (gr.next()) {\n        if (canonical(JSON.parse(gr.getValue('payload'))) !== canonical(payload)) { fail(409, 'Conflicting duplicate'); return; }\n        reply(200, { event_id: eventId, record_id: gr.getUniqueValue(), state: gr.getValue('state'), duplicate: true }); return;\n      }\n      c = contract();\n      gr.initialize(); gr.setValue('event_id', eventId); gr.setValue('payload', JSON.stringify(payload));\n      gr.setValue('state', 'queued'); gr.setValue('contract_ref', c.id);\n      var eventRecord = gr.insert();\n      if (!eventRecord) throw new Error('Event insert failed');\n      reply(202, { event_id: eventId, record_id: String(eventRecord), state: 'queued', duplicate: false }); return;\n    }\n    eventId = request.pathParams.event_id;\n    gr = find('x_eq_demo_event', 'event_id', eventId);\n    if (!gr.next()) { fail(404, 'Unknown event'); return; }\n    payload = JSON.parse(gr.getValue('payload'));\n    if (mode === 'process' && gr.getValue('state') !== 'processed') {\n      c = contract();\n      var delivery = new GlideRecord('x_eq_demo_delivery'); delivery.initialize();\n      delivery.setValue('event', gr.getUniqueValue()); delivery.setValue('order_id', payload.order_id);\n      delivery.setValue('amount', payload.amount); delivery.setValue('currency', payload.currency);\n      var route = payload.amount < c.threshold ? 'standard' : 'priority'; delivery.setValue('route', route);\n      var deliveryId = delivery.insert();\n      if (!deliveryId) throw new Error('Delivery insert failed');\n      gr.setValue('state', 'processed'); gr.setValue('route', route); gr.setValue('delivery', deliveryId);\n      if (!gr.update()) throw new Error('Event update failed');\n    }\n    var result = { event_id: eventId, state: gr.getValue('state'),\n      route: gr.getValue('route') || null, delivery_id: gr.getValue('delivery') || null };\n    if (mode === 'read') {\n      result.record_id = gr.getUniqueValue(); result.event_type = 'order.created'; result.payload = payload;\n    }\n    reply(200, result);\n  } catch (error) { fail(500, String(error.message || error)); }\n})(request, response);\n",
        "webServiceDefinition": "$api",
        "sysScope": "$scope",
        "requiresAuthentication": true,
        "requiresAclAuthorization": false
      }
    }
  ],
  "test": {
    "structural": true
  }
}

The example handles repeated sequential requests. It does not implement transactional exactly-once delivery under concurrency or partial write failure. Review access policy and add concurrency, recovery and ordinary-user tests before adapting it for production.

1. Create and inspect

Use an authorized development target named dev. Download the manifest into a durable project folder. Choose a fresh scope: replace every occurrence of x_eq_demo, including inside scripts, before the first execution. Keep that prefix and the local results stable on subsequent runs.

sn preflight manifest.sn.json
sn plan manifest.sn.json
sn execute manifest.sn.json --target dev --yes
sn validate manifest.sn.json --target dev
sn test manifest.sn.json --target dev

These lifecycle checks inspect supported configuration. They do not run the separate 25-case benchmark. Set up your instance first if needed.

2. Seed the routing contract

Inspect the contract table first. Create the following row only if no matching contract exists. If one exists, reuse its sys_id; do not insert another. Stop and resolve ambiguity if more than one exists. Replace table prefixes in these commands if you changed the scope.

sn get x_eq_demo_contract 'type=order.created' --target dev --fields sys_id,type,threshold,active --json
contract.json
{"type":"order.created","threshold":10000,"active":true}
sn rest POST /api/now/table/x_eq_demo_contract --body contract.json --target dev --yes

3. Submit, process and read back

Read the API record and identify the one in your application scope. Use its persisted base_uri as the API base; do not guess it. Set API_BASE to that relative path, without the instance host or trailing slash.

sn get sys_ws_definition 'service_id=event_queue' --fields sys_id,sys_scope,base_uri --target dev --json
# Set API_BASE to the selected record's relative base_uri.
API_BASE='/api/REPLACE_WITH_ACTUAL_BASE'
event.json
{"event_id":"demo-001","event_type":"order.created","payload":{"order_id":"order-001","amount":7500,"currency":"USD"}}
sn rest POST "$API_BASE/events" --body event.json --target dev --yes
sn rest POST "$API_BASE/process/demo-001" --body-inline '{}' --target dev --yes
sn rest GET "$API_BASE/events/demo-001" --target dev
sn get x_eq_demo_delivery 'order_id=order-001' --target dev --json

Expected: the 7500 order routes standard under the 10000 threshold. Repeat the same requests sequentially and inspect the stored rows: the event and delivery identities should stay the same, with one delivery.

4. Rerun. Then change the requirement.

sn execute manifest.sn.json --target dev --yes
sn validate manifest.sn.json --target dev
sn drift manifest.sn.json --target dev

Compare captured configuration and record identities with the first run. Update the existing contract threshold to 5000 using its saved sys_id. This is a runtime data change, separate from the manifest deployment.

sn rest PATCH /api/now/table/x_eq_demo_contract/CONTRACT_SYS_ID --body-inline '{"threshold":5000}' --target dev --yes

Submit a new event and order identity with amount 7500, then process it: the new delivery should route priority. Read both deliveries to verify the original is unchanged. For changes to shipped configuration, use sn patch and a new update set.

Observed

Working software. Inspectable results.

Event Queue receives orders, stores events and routes deliveries using a configurable threshold. These checks inspected persisted records on one development instance.

25

functional cases passed

Duplicate requests, malformed payloads, processing and stored results.

286/286

retained-app assertions

Existing identities and contents preserved in a separate change-mode evaluation.

27

security records captured

Three roles, 12 ACLs and 12 role links in a separate four-table control.