Hooks: Flow Control and Automation

Hooks are a powerful SDK feature that enable full workflow customization, including side-effects, intercepts, and redirects.

Overview

Hooks are a core SDK tool that enable you to add logic at any step in your workflow, like automatic approvals/denials, side effects in other systems, and so forth.

Every core Sym event comes with a set of hooks:

  • An on-Hook (e.g. on_request) that executes before the event
  • An after-Hook (e.g. after_escalate) that executes after the event

In practice, not every hook will be useful for every workflow. Below is a selection of some of the most common implementations.

Implementation

Hooks are all defined inside of a Sym Approval Flow's impl.py file, fire at specific points in the workflow, and can deliver a broad range of user-defined behaviors.

Hooks prefixed with on_ will fire after a relevant event has been triggered, but before it is processed. For example, an on_request Hook will trigger after a request form is submitted; but before the get_approvers Reducer is processed and a channel message is sent.

Hooks prefixed with after_ will fire after a relevant event has finished processing.

πŸ“˜

Recursion is tightly gated

In theory, a Hook can return its own calling event (e.g. return ApprovalTemplate.request() inside of on_request). To prevent unintended timeouts due to user behavior, the Sym SDK will process the first recursive call only.

In other words, any recursive returns in Hooks should be written with the expectation that they will be processed exactly once, after which either a different outcome should be processed, or the Flow will terminate.

Determining if an event came from a Hook

You may want to split your logic based on the source of an event. For example, if you want to gate approvals to a specific list of people, but always allow SDK automations to pass through. In such cases, you'll want to inspect event.channel.type, as in the following example.

@hook
def on_approve(event):
    """
    Only let members of the approver safelist approve requests
    """
    # Don't check approval permissions if the event came from another hook
    if event.channel.type == "sdk":
        return

    if not has_approve_access(event):
        return ApprovalTemplate.ignore(
            message="You are not authorized to approve this request."
        )

For more information about event attributes, please see Working With Flow Fields and Data.

Examples of common Hooks

on_prompt

on_prompt Hooks will fire before a user even sees the data entry form for a Flow. This Hook might be useful if you want to block certain users or user groups from even requesting to run certain Flows.

@hook
def on_prompt(event):
  if event.flow.srn.slug == "super_secret_flow" and event.user.email != "[email protected]":
    return ApprovalTemplate.ignore(message="You're not allowed to access the super secret Flow!")

on_request

One of the most useful Hooks,on_request fires right as a user submits their request. This is the best place to do deep data validation, as well as automate approvals and denials. For more examples, see Automating and Fast-Tracking Approvals

@hook
def on_request(event):
  '''
  Auto-approve urgent requests for
  access by the person on call
  '''
  if pagerduty.is_on_call(event.user):    
    return ApprovalTemplate.approve()

after_request

This Hook is useful if, after a request is made, you want to reflect its existence in another system, e.g. your team's shared bug tracker.

@hook
def after_request(event):
  # Send a text to everyone on call when a request is made
  for user in pagerduty.users_on_call(escalation_policy_name="123"):
    requests.post(
      url="http://my-website.com/sms",
      body={"message": f"{event.user.email} just made an access request!"}
    )

on_approve

on_approve is the most commonly used Hook in Sym's SDK. It fires as a user "Approves" a request. For more information, see Defining Who Can Approve Requests

@hook
def on_approve(event):
  if not okta.is_user_in_group(event.user, group_id=event.flow.vars["okta_managers_group"]):
    return ApprovalTemplate.ignore(message="Only managers may approve access requests.")

on_deny

This Hook is effectively the same as on_approve-- the two often go hand in hand.

@hook
def on_deny(event):
  if okta.is_user_in_group(event.user, group_id=event.flow.vars["okta_interns_group"]):
    return ApprovalTemplate.ignore(message="Interns cannot deny access requests.")

after_escalate

after_escalate is the natural partner to after_request, in that it can be used to follow up on any side-affecting audit trails that an action has taken place via Sym.

@hook
def after_escalate(event):
  requester = event.get_actor("request")
  # Send a text to the requester when their access is granted
  requests.post(
    url="http://my-website.com/sms",
    body={
      "message": f"Your access to {event.flow.srn.slug} has been granted!",
      "user_email": requester.email
    }
  )

on_revoke

By default, only the requester, approver, or admins may revoke access. This default behavior can be overridden with a custom on_revoke hook. For example:

@hook
def on_revoke(event):
  if not okta.is_user_in_group(event.user, group_id=event.flow.vars["okta_managers_group"]):
    return ApprovalTemplate.ignore(message="Only managers may revoke access requests.")

If you want to extend the default behavior without fully overriding it, you can add checks for requestor, approver, and admin roles as follows:

@hook
def on_revoke(event):
    requester = event.get_actor("request")
    approver = event.get_actor("approve")

    if event.user not in {requester, approver} and event.user.role != "admin":
        return ApprovalTemplate.ignore(message="Only the requester, approver, or admins may revoke access.")

after_deescalate

Similar to after_escalate, this Hook can be useful when paper-trailing requests and escalations in third party systems.

@hook
def after_deescalate(event):
  # Send a text to the requester when their access has expired
  requests.post(
    url="http://my-website.com/sms",
    body={
      "message": f"Your access to {event.flow.srn.slug} has expired!",
      "user_email": event.user.email
    }
  )