Axon syntax

This page shows the canonical Axon authoring syntax with practical examples.

Create Axon workflow

Workflow declaration

workflow Name@v1(input: InputType) -> OutputType {
  return input
}

A workflow has a name, version, one input parameter, an optional output type, and a block.

type FindingInput {
  finding_id: String;
  severity: String;
  summary: String;
  channel: String;
}

workflow NotifyFinding@v1(input: FindingInput) -> Object {
  return {
    finding_id: input.finding_id,
    notified: true
  }
}

Types

Use type to define structured inputs and outputs.

type EmailTarget {
  address: String;
  display_name: String?;
}

type NotificationInput {
  subject: String;
  body: String;
  recipients: [EmailTarget];
}

Common types include String, Number, Boolean, Object, Json, arrays such as [String], and optional fields with ?.

Variables and assignment

Use let for new bindings and plain assignment for reassignment.

workflow ScoreFinding@v1(input: Object) -> Object {
  let score = 0

  if input.severity == "critical" {
    score = 100
  } else {
    score = 50
  }

  return { score: score }
}

Objects and arrays

Object fields use :.

let payload = {
  title: input.summary,
  labels: ["mach5", "needs-review"],
  metadata: {
    finding_id: input.finding_id,
    severity: input.severity
  }
}

Templates

Template strings use backticks and MiniJinja syntax.

let message = `Finding {{ input.finding_id }} is {{ input.severity }}: {{ input.summary }}`

Multi-line templates are useful for issue bodies and email text.

let body = `
Mach5 finding: {{ input.finding_id }}

Severity: {{ input.severity }}
Summary: {{ input.summary }}
`

Connector effect syntax

Use effect connector to call a configured integration.

let output = effect connector {
  connection: "connection-name",
  operation: "operation_name",
  input: {
    key: "value"
  }
}

The fields are:

FieldRequiredMeaning
connectionYesName of the configured connector connection.
operationYesConnector operation name. The connector prefix is optional when the connection kind is known.
inputYesJSON-compatible operation input.

Example: post to Slack

type SlackFindingInput {
  channel: String;
  finding_id: String;
  severity: String;
  summary: String;
}

workflow PostFindingToSlack@v1(input: SlackFindingInput) -> Object {
  let text = `*{{ input.severity }}* finding {{ input.finding_id }}: {{ input.summary }}`

  let result = effect connector {
    connection: "slack-prod",
    operation: "post_message",
    input: {
      channel: input.channel,
      text: text
    }
  } with {
    idempotency_key: `{{ input.finding_id }}:slack_post`
  }

  return {
    finding_id: input.finding_id,
    slack_result: result
  }
}

Example: create a GitHub issue

type GitHubIssueInput {
  owner: String;
  repo: String;
  finding_id: String;
  title: String;
  body: String;
}

workflow CreateGitHubIssue@v1(input: GitHubIssueInput) -> Object {
  let issue = effect connector {
    connection: "github-prod",
    operation: "create_issue",
    input: {
      owner: input.owner,
      repo: input.repo,
      title: input.title,
      body: input.body,
      labels: ["mach5", "investigation"]
    }
  } with {
    idempotency_key: `{{ input.finding_id }}:github_issue`
  }

  return issue
}

Example: Okta lookup then conditional action

type OktaReviewInput {
  login: String;
  disable: Boolean;
  reason: String;
}

workflow ReviewOktaUser@v1(input: OktaReviewInput) -> Object {
  let user = effect connector {
    connection: "okta-prod",
    operation: "lookup_user",
    input: {
      login: input.login
    }
  }

  if input.disable {
    let action = effect connector {
      connection: "okta-prod",
      operation: "deactivate_user",
      input: {
        user_id: user.id,
        reason: input.reason
      }
    } with {
      approval: "required",
      idempotency_key: `{{ input.login }}:deactivate`
    }

    return {
      user: user,
      action: action
    }
  }

  return {
    user: user,
    action: "review_only"
  }
}

Example: send email through SMTP

type EmailInput {
  to: [String];
  subject: String;
  text: String;
}

workflow SendEmail@v1(input: EmailInput) -> Object {
  let result = effect connector {
    connection: "smtp-prod",
    operation: "send_email",
    input: {
      to: input.to,
      subject: input.subject,
      text: input.text
    }
  } with {
    idempotency_key: `email:{{ input.subject }}`
  }

  return result
}

Control flow

If / else

if input.severity == "critical" {
  log warn `Critical finding {{ input.finding_id }}`
} else {
  log info `Finding {{ input.finding_id }} queued`
}

Match

let priority = match input.severity {
  "critical" => "P1",
  "high" => "P2",
  "medium" => "P3",
  _ => "P4"
}

Foreach

foreach channel in input.channels {
  effect connector {
    connection: "slack-prod",
    operation: "post_message",
    input: {
      channel: channel,
      text: input.text
    }
  }
}

Use checkpoint_each_iter when each iteration should be restart-safe.

foreach recipient in input.recipients checkpoint_each_iter {
  effect connector {
    connection: "smtp-prod",
    operation: "send_email",
    input: {
      to: [recipient],
      subject: input.subject,
      text: input.text
    }
  }
}

Parallel branches

workflow NotifyEverywhere@v1(input: Object) -> Object {
  let slack = null
  let email = null

  parallel join all {
    branch {
      slack = effect connector {
        connection: "slack-prod",
        operation: "post_message",
        input: {
          channel: input.channel,
          text: input.message
        }
      }
    }
    branch {
      email = effect connector {
        connection: "smtp-prod",
        operation: "send_email",
        input: {
          to: input.recipients,
          subject: input.subject,
          text: input.message
        }
      }
    }
  }

  return {
    slack: slack,
    email: email
  }
}

Error handling

Use try, catch, and finally for recoverable failures.

workflow SafeSlackPost@v1(input: Object) -> Object {
  try {
    let result = effect connector {
      connection: "slack-prod",
      operation: "post_message",
      input: {
        channel: input.channel,
        text: input.text
      }
    }

    return { ok: true, result: result }
  } catch as err {
    log error `Slack post failed: {{ err }}`
    return { ok: false, error: err }
  } finally {
    log info `SafeSlackPost completed`
  }
}

Built-in effects

Sleep

effect sleep { duration: "30s" }

Run a child workflow

let child = effect run_workflow {
  workflow: "PostFindingToSlack@v1",
  input: {
    channel: input.channel,
    finding_id: input.finding_id,
    severity: input.severity,
    summary: input.summary
  }
}

Emit an event

effect emit_event {
  topic: "finding.reviewed",
  payload: {
    finding_id: input.finding_id,
    status: "reviewed"
  }
}

Policy with with

Any effect or assignment can carry a policy object with with { ... }.

let result = effect connector {
  connection: "github-prod",
  operation: "merge_pull_request",
  input: {
    owner: input.owner,
    repo: input.repo,
    pull_number: input.pull_number
  }
} with {
  approval: "required",
  timeout: "5m",
  idempotency_key: `{{ input.owner }}/{{ input.repo }}#{{ input.pull_number }}:merge`
}

Common policy fields include approval requirements, timeouts, retry settings, and idempotency keys. Exact policy enforcement depends on the workflow runtime configuration.

Operation names

Connector operation names can be unprefixed or prefixed by connector kind.

operation: "post_message"
operation: "slack.post_message"

Both resolve to the same Slack operation when the connection kind is slack.

Next steps

Analytics Cookies

Help us understand website usage.

Necessary storage remembers your choice. With your consent, Mach5 also uses PostHog analytics to measure website traffic and interactions.

Change this anytime from Cookie Settings in the footer. Privacy Notice.