FortiAnalyzer playbook variables: How to help yourself

As I was preparing for Accelerate 2026 and the Ultimate Fabric Challenge, one of the topics I wanted to focus on a lot more was Security Operations (SecOps), because this is an area I was always weak in.

SecOps, in this context, means working with FortiAnalyzer automation, i.e. event handlers, playbooks, incidents and connectors. A part I really wanted to immerse myself in was how FortiAnalyzer handles variables, and in the process, I think I gained some useful knowledge that I now want to show the world. So, dear reader, please, read on, and maybe this proves useful to you. It was relatively quick for me to complete, but it’s not going to be short, though there are lots of pictures!

The setup

For this post, I have the following components:

  • FortiAnalyzer running 7.6.6
  • FortiGate running 7.6.6
  • FortiAuthenticator running 8.0.1
  • An Ubuntu server to receive some POST requests

A primer on playbooks

FortiAnalyzer uses playbooks to automate various workflows using connectors that integrate other systems, like FortiGates, FortiClient EMS, FortiAuthenticator, etc. Every playbook uses a trigger, which is usually an event or an incident, but you could also run it on demand or on a schedule. After this trigger various tasks are executed that define what connector is used, various parameters, and variables, to accomplish, well, lots of things.

Playbooks can be simple, only sending a mail with information from an event, and fully orchestrate an incident response flow, by triggering on an event, creating an incident, running a report, banning IPs, sending mails, and you could go on and on, really. For this post, we’ll keep it simple and focus on how to work with variables.

Setting the stage

For the first type of variables, we’ll look at connectors and, in this case, specifically, the one for FortiAuthenticator (FAC). The FAC connector is one that works with HTTP-based communication, so whatever FortiAnalyzer sends to FAC can be easily read in cleartext.

The FAC connector has a few actions, and for our example, we’ll look at two:

  1. Get User List, which fetches a type of user (LDAP, RADIUS, or local) and saves it in a list
  2. Update User Status, which we will use to deactivate a user using an ID

I have prepared the following playbook:

FortiAnalyzer FortiAuthenticator playbook

There are four tasks in this:

  1. An On Demand trigger
  2. The Get User List task, using the FAC connector, to get the user list
  3. The Update User Status task, using the FAC connector, to deactivate a user by ID
  4. A generic webhook task that sends an HTTP POST request

The generic webhook is what is used to demonstrate one way we can check what FortiAnalyzer sends.

The configuration of the webhook looks like this:

FortiAnalyzer generic webhook

The protocol being HTTP is important.

The target of the webhook is the Ubuntu server that uses http-echo-server to simply echo any incoming POST requests to the terminal.

[Update 2026-03-26]: Chris Eddisford from Fortinet has shown me webhook.site, which you can use as a generic webhook receiver, so you can send your POST requests to your unique URL and immediately see the result. No sign-up or Ubuntu server needed. Thanks a lot, Chris!

Here is the configuration of all tasks, except the On Demand trigger, because there isn’t anything to show for that one:

If I run this playbook as is, it won’t actually do anything on FAC, because the Update User Status task expects a specific user ID, and not a list of users, but we’ll get to that. At this point, we care about what FortiAnalyzer sends.

Note: I want to stress that this playbook exists purely as an exercise to show the process. In reality, you wouldn’t actually do this. In a production environment, you’d get the username from a triggered event, pass the username to the Get User action to retrieve the user ID and then use the Update User Status action with the user ID as your input.

Getting the variable values from HTTP-based connectors

If I run this playbook, the playbook will run successfully, and my Ubuntu server will get a POST request.

FortiAnalyzer playbook execution

Ubuntu POST echo

[server] event: listening (port: 8081)
[server] event: connection (socket#1)
[socket#1] event: resume
[socket#1] event: data
--> POST / HTTP/1.1
--> Host: 192.168.1.151:8081
--> Accept-Encoding: identity
--> User-Agent: python-urllib3/2.3.0
--> Content-Length: 1164
-->
--> {"meta": {"limit": 10, "next": null, "offset": 0, "previous": null, "total_count": 2}, "objects": [{"active": true, "company": null, "department": null, "dn": "CN=manager,OU=USERS,OU=LAB,DC=ad,DC=labdomain,DC=com", "email": "", "fido": false, "first_name": "manager", "ftm_act_method": "", "id": 2, "is_locked": false, "last_name": "", "mobile_number": "", "reason": null, "recovery_by_question": false, "resource_uri": "/api/v1/ldapusers/2/", "server_address": "win-ad.ad.labdomain.com", "server_name": "WIN-AD", "token_auth": false, "token_serial": "", "token_type": "", "username": "manager@ad.labdomain.com"}, {"active": false, "company": "LAB", "department": "NetSec", "dn": "CN=adkevin,OU=USERS,OU=LAB,DC=ad,DC=labdomain,DC=com", "email": "adkevin@ad.labdomain.com", "fido": false, "first_name": "adkevin", "ftm_act_method": "email", "id": 1, "is_locked": false, "last_name": "LAB", "mobile_number": "", "reason": 0, "recovery_by_question": false, "resource_uri": "/api/v1/ldapusers/1/", "server_address": "win-ad.ad.labdomain.com", "server_name": "WIN-AD", "token_auth": false, "token_serial": "", "token_type": "", "username": "adkevin@ad.labdomain.com"}]}
[socket#1] event: prefinish
[socket#1] event: finish
[socket#1] event: readable
[socket#1] event: end
[socket#1] event: close

In the POST request we see that FortiAnalyzer uses a Python library to send the request (as a matter of fact, playbooks in general use Python, which you see clearly if you have an error), and we get the JSON payload consisting of two important dictionaries: meta, which has, as the name says, meta information, and objects, which holds a list of dictionaries that have the actual entries that were retrieved from FAC.

You can paste the entire payload into your favourite JSON formatter, like JSON formatter, to make it more readable.

JSON formatter view

But let’s assume you don’t have a Linux server with this http-echo-server available, and you want to get the information using only FortiAnalyzer. Don’t worry, this requires only marginally more work.

Important point: Before FortiAnalyzer sends a POST, it checks if it can connect to the destination over the specified port (it doesn’t matter if the port can do something with the payload), so if you don’t have a target with an open port, this won’t work. Luckily, FortiAnalyzer is definitely available, so you can use that as your target in the webhook connector as well as the task and send an HTTP POST to FortiAnalyzer.

First, start a sniffer on FortiAnalyzer that captures Ethernet data. Using my generic connector, my sniffer looks like this:

diagnose sniffer packet any ‘host 192.168.1.151 and port 8081’ 3 0 a

Once that is set up, run the playbook again and look at the output of the sniffer to find the payload, which starts with the meta dictionary.

FortiAnalyzer sniffer output

2026-03-23 18:53:44.784960 192.168.1.151.8081 -> 192.168.1.171.57014: psh 154846471 ack 44744771
0x0000   0000 0000 0001 000c 2931 3adc 0800 4500        ........)1:...E.
0x0010   05c9 398d 4000 4006 770f c0a8 0197 c0a8        ..9.@.@.w.......
0x0020   01ab 1f91 deb6 093a c507 02aa c043 5018        .......:.....CP.
0x0030   0209 ff1a 0000 4461 7465 3a20 4d6f 6e20        ......Date:.Mon.
0x0040   4d61 7220 3233 2032 3032 3620 3138 3a35        Mar.23.2026.18:5
0x0050   333a 3434 2047 4d54 2b30 3030 3020 2843        3:44.GMT+0000.(C
0x0060   6f6f 7264 696e 6174 6564 2055 6e69 7665        oordinated.Unive
0x0070   7273 616c 2054 696d 6529 0d0a 436f 6e6e        rsal.Time)..Conn
0x0080   6563 7469 6f6e 3a20 636c 6f73 650d 0a43        ection:.close..C
0x0090   6f6e 7465 6e74 2d54 7970 653a 2074 6578        ontent-Type:.tex
0x00a0   742f 706c 6169 6e0d 0a41 6363 6573 732d        t/plain..Access-
0x00b0   436f 6e74 726f 6c2d 416c 6c6f 772d 4f72        Control-Allow-Or
0x00c0   6967 696e 3a20 2a0d 0a0d 0a50 4f53 5420        igin:.*....POST.
0x00d0   2f20 4854 5450 2f31 2e31 0d0a 486f 7374        /.HTTP/1.1..Host
0x00e0   3a20 3139 322e 3136 382e 312e 3135 313a        :.192.168.1.151:
0x00f0   3830 3831 0d0a 4163 6365 7074 2d45 6e63        8081..Accept-Enc
0x0100   6f64 696e 673a 2069 6465 6e74 6974 790d        oding:.identity.
0x0110   0a55 7365 722d 4167 656e 743a 2070 7974        .User-Agent:.pyt
0x0120   686f 6e2d 7572 6c6c 6962 332f 322e 332e        hon-urllib3/2.3.
0x0130   300d 0a43 6f6e 7465 6e74 2d4c 656e 6774        0..Content-Lengt
0x0140   683a 2031 3136 340d 0a0d 0a7b 226d 6574        h:.1164....{"met
0x0150   6122 3a20 7b22 6c69 6d69 7422 3a20 3130        a":.{"limit":.10
0x0160   2c20 226e 6578 7422 3a20 6e75 6c6c 2c20        ,."next":.null,.
0x0170   226f 6666 7365 7422 3a20 302c 2022 7072        "offset":.0,."pr
0x0180   6576 696f 7573 223a 206e 756c 6c2c 2022        evious":.null,."
0x0190   746f 7461 6c5f 636f 756e 7422 3a20 327d        total_count":.2}
0x01a0   2c20 226f 626a 6563 7473 223a 205b 7b22        ,."objects":.[{"
0x01b0   6163 7469 7665 223a 2074 7275 652c 2022        active":.true,."
0x01c0   636f 6d70 616e 7922 3a20 6e75 6c6c 2c20        company":.null,.
0x01d0   2264 6570 6172 746d 656e 7422 3a20 6e75        "department":.nu
0x01e0   6c6c 2c20 2264 6e22 3a20 2243 4e3d 6d61        ll,."dn":."CN=ma
0x01f0   6e61 6765 722c 4f55 3d55 5345 5253 2c4f        nager,OU=USERS,O
0x0200   553d 4c41 422c 4443 3d61 642c 4443 3d6c        U=LAB,DC=ad,DC=l
0x0210   6162 646f 6d61 696e 2c44 433d 636f 6d22        abdomain,DC=com"
0x0220   2c20 2265 6d61 696c 223a 2022 222c 2022        ,."email":."",."
0x0230   6669 646f 223a 2066 616c 7365 2c20 2266        fido":.false,."f
0x0240   6972 7374 5f6e 616d 6522 3a20 226d 616e        irst_name":."man
0x0250   6167 6572 222c 2022 6674 6d5f 6163 745f        ager",."ftm_act_
0x0260   6d65 7468 6f64 223a 2022 222c 2022 6964        method":."",."id
0x0270   223a 2032 2c20 2269 735f 6c6f 636b 6564        ":.2,."is_locked
0x0280   223a 2066 616c 7365 2c20 226c 6173 745f        ":.false,."last_
0x0290   6e61 6d65 223a 2022 222c 2022 6d6f 6269        name":."",."mobi
0x02a0   6c65 5f6e 756d 6265 7222 3a20 2222 2c20        le_number":."",.
0x02b0   2272 6561 736f 6e22 3a20 6e75 6c6c 2c20        "reason":.null,.
0x02c0   2272 6563 6f76 6572 795f 6279 5f71 7565        "recovery_by_que
0x02d0   7374 696f 6e22 3a20 6661 6c73 652c 2022        stion":.false,."
0x02e0   7265 736f 7572 6365 5f75 7269 223a 2022        resource_uri":."
0x02f0   2f61 7069 2f76 312f 6c64 6170 7573 6572        /api/v1/ldapuser
0x0300   732f 322f 222c 2022 7365 7276 6572 5f61        s/2/",."server_a
0x0310   6464 7265 7373 223a 2022 7769 6e2d 6164        ddress":."win-ad
0x0320   2e61 642e 6c61 6264 6f6d 6169 6e2e 636f        .ad.labdomain.co
0x0330   6d22 2c20 2273 6572 7665 725f 6e61 6d65        m",."server_name
0x0340   223a 2022 5749 4e2d 4144 222c 2022 746f        ":."WIN-AD",."to
0x0350   6b65 6e5f 6175 7468 223a 2066 616c 7365        ken_auth":.false
0x0360   2c20 2274 6f6b 656e 5f73 6572 6961 6c22        ,."token_serial"
0x0370   3a20 2222 2c20 2274 6f6b 656e 5f74 7970        :."",."token_typ
0x0380   6522 3a20 2222 2c20 2275 7365 726e 616d        e":."",."usernam
0x0390   6522 3a20 226d 616e 6167 6572 4061 642e        e":."manager@ad.
0x03a0   6c61 6264 6f6d 6169 6e2e 636f 6d22 7d2c        labdomain.com"},
0x03b0   207b 2261 6374 6976 6522 3a20 6661 6c73        .{"active":.fals
0x03c0   652c 2022 636f 6d70 616e 7922 3a20 224c        e,."company":."L
0x03d0   4142 222c 2022 6465 7061 7274 6d65 6e74        AB",."department
0x03e0   223a 2022 4e65 7453 6563 222c 2022 646e        ":."NetSec",."dn
0x03f0   223a 2022 434e 3d61 646b 6576 696e 2c4f        ":."CN=adkevin,O
0x0400   553d 5553 4552 532c 4f55 3d4c 4142 2c44        U=USERS,OU=LAB,D
0x0410   433d 6164 2c44 433d 6c61 6264 6f6d 6169        C=ad,DC=labdomai
0x0420   6e2c 4443 3d63 6f6d 222c 2022 656d 6169        n,DC=com",."emai
0x0430   6c22 3a20 2261 646b 6576 696e 4061 642e        l":."adkevin@ad.
0x0440   6c61 6264 6f6d 6169 6e2e 636f 6d22 2c20        labdomain.com",.
0x0450   2266 6964 6f22 3a20 6661 6c73 652c 2022        "fido":.false,."
0x0460   6669 7273 745f 6e61 6d65 223a 2022 6164        first_name":."ad
0x0470   6b65 7669 6e22 2c20 2266 746d 5f61 6374        kevin",."ftm_act
0x0480   5f6d 6574 686f 6422 3a20 2265 6d61 696c        _method":."email
0x0490   222c 2022 6964 223a 2031 2c20 2269 735f        ",."id":.1,."is_
0x04a0   6c6f 636b 6564 223a 2066 616c 7365 2c20        locked":.false,.
0x04b0   226c 6173 745f 6e61 6d65 223a 2022 4c41        "last_name":."LA
0x04c0   4222 2c20 226d 6f62 696c 655f 6e75 6d62        B",."mobile_numb
0x04d0   6572 223a 2022 222c 2022 7265 6173 6f6e        er":."",."reason
0x04e0   223a 2030 2c20 2272 6563 6f76 6572 795f        ":.0,."recovery_
0x04f0   6279 5f71 7565 7374 696f 6e22 3a20 6661        by_question":.fa
0x0500   6c73 652c 2022 7265 736f 7572 6365 5f75        lse,."resource_u
0x0510   7269 223a 2022 2f61 7069 2f76 312f 6c64        ri":."/api/v1/ld
0x0520   6170 7573 6572 732f 312f 222c 2022 7365        apusers/1/",."se
0x0530   7276 6572 5f61 6464 7265 7373 223a 2022        rver_address":."
0x0540   7769 6e2d 6164 2e61 642e 6c61 6264 6f6d        win-ad.ad.labdom
0x0550   6169 6e2e 636f 6d22 2c20 2273 6572 7665        ain.com",."serve
0x0560   725f 6e61 6d65 223a 2022 5749 4e2d 4144        r_name":."WIN-AD
0x0570   222c 2022 746f 6b65 6e5f 6175 7468 223a        ",."token_auth":
0x0580   2066 616c 7365 2c20 2274 6f6b 656e 5f73        .false,."token_s
0x0590   6572 6961 6c22 3a20 2222 2c20 2274 6f6b        erial":."",."tok
0x05a0   656e 5f74 7970 6522 3a20 2222 2c20 2275        en_type":."",."u
0x05b0   7365 726e 616d 6522 3a20 2261 646b 6576        sername":."adkev
0x05c0   696e 4061 642e 6c61 6264 6f6d 6169 6e2e        in@ad.labdomain.
0x05d0   636f 6d22 7d5d 7d                              com"}]}

Copy the payload from meta to the curly brackets at the end to your text editor of choice (mine is Notepad++) and perform the following find and replace actions:

  1. Replace \r\n (line breaks) with no character using the Extended function
  2. Replace :. (colon dot) with . (dot)
  3. Replace :, (comma dot) with , (comma)

After that, you should have the entire payload on a single line, and you can, again, paste it into a JSON formatter.

Here is a short video where I show this process if you want to see it in action:

With the JSON information available, we can easily find out what information has been retrieved, how it’s structured, and how it can be further used.

JSON payload

{
  "meta": {
    "limit": 10,
    "next": null,
    "offset": 0,
    "previous": null,
    "total_count": 2
  },
  "objects": [
    {
      "active": true,
      "company": null,
      "department": null,
      "dn": "CN=manager,OU=USERS,OU=LAB,DC=ad,DC=labdomain,DC=com",
      "email": "",
      "fido": false,
      "first_name": "manager",
      "ftm_act_method": "",
      "id": 2,
      "is_locked": false,
      "last_name": "",
      "mobile_number": "",
      "reason": null,
      "recovery_by_question": false,
      "resource_uri": "/api/v1/ldapusers/2/",
      "server_address": "win-ad.ad.labdomain.com",
      "server_name": "WIN-AD",
      "token_auth": false,
      "token_serial": "",
      "token_type": "",
      "username": "manager@ad.labdomain.com"
    },
    {
      "active": false,
      "company": "LAB",
      "department": "NetSec",
      "dn": "CN=adkevin,OU=USERS,OU=LAB,DC=ad,DC=labdomain,DC=com",
      "email": "adkevin@ad.labdomain.com",
      "fido": false,
      "first_name": "adkevin",
      "ftm_act_method": "email",
      "id": 1,
      "is_locked": false,
      "last_name": "LAB",
      "mobile_number": "",
      "reason": 0,
      "recovery_by_question": false,
      "resource_uri": "/api/v1/ldapusers/1/",
      "server_address": "win-ad.ad.labdomain.com",
      "server_name": "WIN-AD",
      "token_auth": false,
      "token_serial": "",
      "token_type": "",
      "username": "adkevin@ad.labdomain.com"
    }
  ]
}

Using my payload, I can get the ID for the user “adkevin@ad.labdomain.com” by accessing objects[1][‘id’].

With this information, I can now change the Update User Status task accordingly.

Putting the pieces together

When you edit a task that includes variables, you have two methods to set variables:

  1. Select them from a selector menu
  2. Writing the variable string yourself

You can combine these methods by first selecting a variable, clicking on the “A” and then editing the variable as you need it.

The FAC Update User Status task wants the ID that FAC itself assigned, so if we first select the user_list variable from the previous task, then switch to the manual input, we can now add the information we got previously. This can look like the following:

FortiAnalyzer task to show how to switch from selector to manual input of variables

If we now run the playbook again, the user with the ID we got will now be correctly disabled on FAC, as signalled by the red X in Status.

FortiAuthenticator LDAP user status

This was for HTTP-based connectors, but I also want to show how you can get variable information to use in FortiGate automation stitches.

FortiGate automation stitches and FortiAnalyzer

If a FortiGate logs to FortiAnalyzer and you have configured an automation stitch that uses an Incoming Webhook Call as a trigger, the FortiGate will show up as a connected device in the “FortiOS Connector

FortiAnalyzer FortiOS connector status

For this post, I have configured a basic automation stitch that, as an action, creates an address object and adds it to a group. I have this group as the source in a deny policy, and the idea is that if this stitch is triggered, the offending IP will get automatically blocked.

The trigger, action, and stitch configuration is as follows:

But we’re getting ahead of ourselves here, because in the action picture, I’m using variables, but how do I even know what they are called and what they represent?

Setting the stage for automation stitches

A FortiGate gets the information from FortiAnalyzer in the log variable, which can be accessed using %%log%% in an action. This information is directly available in the GUI by clicking on the % symbol.

FortiGate automation action help

This is already a big help, but it doesn’t help with showing you the entire list of variables that FortiAnalyzer makes available to you, so let’s find out how to get this information.

First, we need to change the action on the FortiGate. By using the %%log%% variable, as shown in the picture above, we can assign this variable a value on FortiAnalyzer with a playbook.

Second, we need a playbook, and in this case, I have a simple one with two tasks:

  1. Trigger on an event
  2. Execute a FortiOS webhook
FortiAnalyzer FortiGate playbook

The trigger matches on the event SSH-GOOGLE, and the event handler simply checks for SSH connections to the Google DNS server.

The FortiOS webhook task has the information about the FortiGate device where this action gets executed, the webhook name, and the variables, which are taken from the automation action.

faz_fgt_play_webhook_log

An important point here is that the variables you use in an action will be dynamically and periodically synced to FortiAnalyzer, meaning that if you change or add variables in an action and wait a bit, FortiAnalyzer will show the updated variables in the task. You see this in two places:

  1. The task itself
  2. By clicking on the “x device(s) connected” message in the FortiOS Connector (see the picture above)

Here is a comparison of how it looks if we use the variables from the action we will use to create and add an object to a group, and the action we use to get the information for the full information.

FortiAnalyzer FortiOS connector status with dynamic variables

The last piece we need, before seeing the values for the variables, is telling FortiAnalyzer what variable to send, and this is done in the FortiOS webhook task.

In this task, we have, much like with the FAC example from earlier, two ways to accomplish this:

  1. Select the variable from a selector menu
  2. Writing the variable string yourself

By selecting the variable, you can go through the entire list of available ones, but if you’re not quite sure what you’re looking for, this is tedious, because you’d need as many variables in the FortiGate’s action as there are variables.

The better method is simply sending everything from the trigger, which is accessed via the variable ${trigger}.

FortiAnalyzer task variable selection or manual entering

With this trigger variable set for the FortiGate’s log variable, we can finally get to finding our values, and this is quite simple.

Getting automation stitch variable values

On the FortiGate, we are going to set up a debug for the automation stitch application, and there we’ll see what is being delivered by FortiAnalyzer. The necessary commands are as follows:

diagnose debug application autod -1
diagnose debug enable

With this prepared, we can trigger the event handler by trying to connect to Google’s 8.8.8.8 DNS via SSH, and after a bit, the debug messages will come in.

FortiGate automation stitch log debug

__action_cli_script_open()-171: cli script action:CREATE-BAD-HOST is called. svc ctx:0x55a1ff3aef80
accprof:super_admin script:
%%log%%

__read_cli_script_result()-117: cli script:
autod.3
 output:

========== #1, 2026-03-23 19:33:24 ==========
FGT02  date=2026-03-23 time=19:33:24 eventtime=1774290803211381959 tz="+0100" logid="0100065301" type="event" subtype="system" level="notice" vd="root" logdesc="Internal Message" path="system" name="automation-stitch" action="webhook" mkey="Incoming Webhook Call" log="{ \"adom_name\": \"LAB\", \"adom_prefix\": \"FSFADOM198\", \"handler_name\": \"SSH-GOOGLE\", \"group_value\": \"192.168.1.231::\", \"event_id\": \"202603231000010013\", \"event_time\": \"1774290774\", \"devid\": \"FGVMSLTM26006351\", \"vdom\": \"root\", \"severity\": 2, \"epid\": \"3\", \"epname\": \"192.168.1.231\", \"epip\": \"192.168.1.231\", \"dst_epid\": \"101\", \"dst_epname\": \"8.8.8.8\", \"dst_epip\": \"8.8.8.8\", \"dvid\": \"1062\", \"euid\": \"3\", \"handler_type\": \"basic\", \"rule_name\": \"SSH-GOOGLE\", \"euname\": \"N\\\/A\", \"subject\": \"srcip:192.168.1.231<G>1:6:13<\\\/G>\", \"groupby1\": \"192.168.1.231\", \"logtype\": \"traffic\", \"devtype\": \"FortiGate\", \"extrainfo\": \"{ }\", \"targets\": [ { \"srcip\": \"192.168.1.231\" }
Unknown action 0

======= end of #1, 2026-03-23 19:33:24 ======

And here we got our variable names and their associated values, right for the picking and inserting into our automation action. Now we can change our script in the FortiGate action to create an object using epip in the name and subnet, putting mkey and date in the comment, and adding the object to our group (see above or the debug below for the full script).

Trigger the event handler again, and everything looks as it should.

FortiGate automation stitch real debug

__action_cli_script_open()-171: cli script action:CREATE-BAD-HOST is called. svc ctx:0x55a1ff3be410
accprof:super_admin script:
config firewall address
edit "H_%%log.epip%%"
set subnet %%log.epip%% 255.255.255.255
set comment "%%log.mkey%%_%%log.date%%"
next
end
config firewall addrgrp
edit G_BAD-GROUP
append member "H_%%log.epip%%"
next
end

__read_cli_script_result()-117: cli script:
autod.2
 output:

========== #1, 2026-03-23 19:25:33 ==========
FGT02  config firewall address
FGT02 (address)  edit "H_192.168.1.231"
FGT02 (H_192.168.1.231)  set subnet 192.168.1.231 255.255.255.255
FGT02 (H_192.168.1.231)  set comment "Incoming Webhook Call_2026-03-23"
FGT02 (H_192.168.1.231)  next
FGT02 (address)  end
FGT02  config firewall addrgrp
FGT02 (addrgrp)  edit G_BAD-GROUP
FGT02 (G_BAD-GROUP)  append member "H_192.168.1.231"
FGT02 (G_BAD-GROUP)  next
FGT02 (addrgrp)  end

======= end of #1, 2026-03-23 19:25:34 ======
FortiGate bad group after automation stitch execution

Wrapping up

I have uploaded the various Fortinet resources from this post (playbooks, event handler and FortiGate configuration) to a GitHub repository, if you want to look at everything yourself. In the future, I will put all blog-related resources in this repository.

I did basically everything here before Accelerate 2026, and it helped me become more confident in SecOps tasks, which was why I put this topic on my to-do list in the first place.

There is definitely more that can be shown regarding this topic, and a better version of this post exists somewhere in a parallel universe, but perfection is something you can strive for; reaching it should never be the goal. My main goal was to cover blind spots that are not mentioned anywhere. I hope this covering of blind posts helps you, dear reader.

Comments

One response to “FortiAnalyzer playbook variables: How to help yourself”

  1. AI Audio Generator avatar

    The point about FortiAnalyzer treating variables differently depending on whether they’re scoped to a playbook or an event handler really clicked for me—I’ve definitely burned time debugging that exact mismatch in the past. Your breakdown of how to reference them inside connectors without hardcoding values is going to save me a lot of headache during our next SecOps tuning pass. Thanks for sharing the “why” behind the syntax, not just the “how.”

    Lyria 3 Pro

Be civil. Stay on topic. Don’t lie.

Leave a Reply

Your email address will not be published. Required fields are marked *