> ## Documentation Index
> Fetch the complete documentation index at: https://klef.ai/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Policy

export const Term = ({term, children}) => {
  const terms = {
    "desired-state": "What should be true for a worker: the accounts and access they should have, given who they are and their lifecycle stage.",
    connector: "An integration with one of your systems.",
    source: "An HRIS Klef pulls its workers from.",
    target: "A connector Klef writes to.",
    "worker-model": "Klef's own record of each worker.",
    worker: "Klef's own record of one person.",
    policy: "A rule that sets the desired state for a group of workers at each lifecycle stage.",
    plan: "The set of operations needed to make reality match desired state, shown before it applies.",
    operation: "A single change described in a plan.",
    reconciler: "The engine that compares desired state to your live systems and produces a plan.",
    resource: "A reusable piece a policy uses: a script, a lookup table, or a secret.",
    segment: "A saved group of workers, defined by conditions over their attributes. A policy applies to one segment, or to everyone.",
    "lifecycle-stage": "Where a worker is right now: Pre-start, Active, Leave, Suspended, or Terminated.",
    mastering: "Taking a worker field's value from a connected system instead of your HRIS."
  };
  return <Tooltip tip={terms[term]}>{children}</Tooltip>;
};

## Overview

A policy is a document that defines the <Term term="desired-state">desired state</Term> for a group of workers. Given a worker's properties and their <Term term="lifecycle-stage">lifecycle stage</Term>, it describes what accounts and access they should have. Klef compares that to the current state of your systems and generates a <Term term="plan">plan</Term> of changes to make reality match the policy.

A policy is written in Klef Policy Language (KPL).

## Anatomy of a Policy

Policies describe **who** they apply to and how their target's objects should look.

```hcl theme={null}
policy "Employee lifecycle" {            # name of the policy
  category = "Company-wide"
  applies  = everyone                    # which workers it applies to

  stage active {                         # the lifecyle stage at which it is applied
    target microsoft_entra.user {        # the target object's desired state
      displayName = worker.display_name
    }

    notify email "welcome" {               # notifications sent to interested parties
      send    = once
      subject = "Welcome"
      body    = "Your first sign-in: {{ microsoft_entra.sign_in_url }}"
      to      = [worker.personal_email]
    }
  }
}
```

## KPL Reference

### `applies`

Either `everyone` or a <Term term="segment">segment</Term>. Segments are a group of workers with something in common. They can be re-used across policies.

```hcl theme={null}
policy "Employee lifecycle" {
  category = "Company-wide"
  applies  = everyone        # <-- Applies to everyone
}
```

### `stage`

A block for one <Term term="lifecycle-stage">lifecycle stage</Term>. A worker is in exactly one stage at a time, and the block for that stage decides what applies to them.

```hcl theme={null}
policy "Employee lifecycle" {
  category = "Company-wide"
  applies  = everyone

  stage active {             # <-- Applies while the worker is active
  }

  stage leave {
  }

  stage terminated {
  }
}
```

Klef determines the worker's stage from its HRIS' status and assignment dates. Their lifecycle is defined as:

| Stage      | Written      | A worker is here when                                                                |
| ---------- | ------------ | ------------------------------------------------------------------------------------ |
| Pre-start  | `pre_start`  | They are hired and but haven't hit their start date yet.                             |
| Active     | `active`     | They are hired and have met their start date.                                        |
| Leave      | `leave`      | On a planned absence, such as parental or medical leave, and are expected back.      |
| Suspended  | `suspended`  | On an involuntary hold.                                                              |
| Departing  | `departing`  | Their last work day is set but has not passed yet. They are still employed.          |
| Terminated | `terminated` | Their last work day has passed, or the HRIS in some other way marks them terminated. |

#### Omitting a Stage

If a policy has no block for a stage, it says nothing about that stage, and the worker's accounts keep whatever they already have.

```hcl theme={null}
policy "Slack access" {
  category = "Company-wide"
  applies  = everyone

  stage active {
    target slack.user {
      userName = worker.business_email
      active   = true
    }
  }

  # <-- No leave block

  stage terminated {
    target slack.user {
      userName = worker.business_email
      active   = false
    }
  }
}
```

As an example, say your policy has an `active` block and a `terminated` block - both modifying a Slack account:

1. The worker goes on parental leave on May 1st, and moves to the `leave` stage.
2. Since the policy has no `leave` block, Klef makes no changes and their Slack account stays active.
3. The worker returns on August 1st, and moves back to the `active` stage. The `active` block applies again.
4. If the worker is instead `terminated`, the `terminated` block applies and their Slack account is deactivated.

### `stage released`

This block, unlike `terminated`, applies when the policy stops covering an account. This can occur if a worker leaves the segment, a target is removed from the policy, or the policy is deleted. It's not a worker lifecycle.

Without it, Klef will apply a reasonable default, such as deactivating the account.

```hcl theme={null}
policy "Employee lifecycle" {
  category = "Company-wide"
  applies  = everyone

  stage released {           # <-- Applies once the policy stops covering the account
    target slack.user {
      userName = worker.business_email
      active   = true
    }
  }
}
```

### `target`

A block naming one object in one <Term term="connector">connector</Term>, with the fields that object should have.

```hcl theme={null}
stage active {
  target microsoft_entra.user {      # <-- The Entra user each worker should have
    userPrincipalName = worker.business_email
    mailNickname      = worker.employee_id
    displayName       = "{{ worker.legal_name.given }} {{ worker.legal_name.family }}"
    accountEnabled    = true
  }
}
```

You do not have to indicate whether it's a create or update. Klef compares the fields you wrote against the live object and does whichever one is needed. Each [connector page](/docs/connectors) lists the objects it accepts and the fields on each.

### Mappings

A line inside a target that sets one field.

```hcl theme={null}
target microsoft_entra.user {
  userPrincipalName = worker.business_email     # <-- Sets userPrincipalName from the worker
  displayName       = "{{ worker.legal_name.given }} {{ worker.legal_name.family }}"
  accountEnabled    = true
  licenses          = ["SPE_E3"]
  jobTitle          = null
  mobilePhone       = unmanaged
}
```

| Written                 | The value                                                                                                                   |
| ----------------------- | --------------------------------------------------------------------------------------------------------------------------- |
| `worker.business_email` | A worker field, copied as it is.                                                                                            |
| `"{{ ... }}"`           | A [Liquid template](https://shopify.github.io/liquid/) with worker fields filled in, so this one arrives as `Ada Lovelace`. |
| `true`                  | A fixed value, the same for every worker.                                                                                   |
| `["SPE_E3"]`            | Picked from what the connector offers, such as licenses or groups. Klef adds and removes to match.                          |
| `null`                  | Klef owns the field and clears it.                                                                                          |
| `unmanaged`             | Klef does not touch the field.                                                                                              |

A value can also come from a script or a lookup table, which is what [resources](#resources) are for.

#### Field Types

| Type        | Written as                                                                            |
| ----------- | ------------------------------------------------------------------------------------- |
| `string`    | Text in quotes, supporting [Liquid template](https://shopify.github.io/liquid/)       |
| `bool`      | `true` or `false`.                                                                    |
| `int`       | A number.                                                                             |
| `date`      | A date from the worker, such as `worker.end_date`.                                    |
| `enum`      | One of the values the connector page lists for the field.                             |
| `reference` | Something that already exists in the target, by its id, name, or address.             |
| `link`      | Another user in the same target, set from the worker's manager.                       |
| `grant`     | A list of what to grant, such as `["All Employees"]`. Klef adds and removes to match. |
| `array`     | A list of rows, each written in `{ }`.                                                |
| `map`       | Entries in `{ }` whose names you choose, such as tags.                                |
| `object`    | Fields in `{ }`, as the connector page lists them.                                    |

#### Templates

Text in quotes is a [Liquid template](https://shopify.github.io/liquid/), so it can read worker fields with `{{ }}` and use Liquid's filters and tags. Text that runs to several lines is written as a heredoc, opened with `<<TAG` and closed by a line holding only the tag.

```hcl theme={null}
target microsoft_entra.user {
  displayName = "{{ worker.legal_name.given }} {{ worker.legal_name.family | upcase }}"   # <-- Ada LOVELACE
  jobTitle    = <<TITLE                                                                   # <-- Engineer, Platform
    {{ worker.job.name }}
    {%- if worker.department.name %}, {{ worker.department.name }}{% endif %}
  TITLE
}
```

### `each`

A mapping value that writes one row for every record in a worker collection, such as `worker.assignments`. It fills an `array` field.

```hcl theme={null}
target oracle_fusion.worker {
  workRelationships = [
    {
      legalEmployerName = "Acme Logistics LLC"
      hireDate          = worker.hire_date

      assignments = each worker.assignments
            as assignment where assignment.status == "Active" {  # <-- One row for each active assignment
        assignmentNumber = assignment.source_id
        assignmentName   = assignment.job.name
        businessUnitName = assignment.business_unit.name
        primaryFlag      = assignment.is_primary
      }
    },
  ]
}
```

`where` is an optional filter. Join clauses with `and` to require all of them, or with `or` to require any of them.

| Operator                   | Matches                               | Example                                    |
| -------------------------- | ------------------------------------- | ------------------------------------------ |
| `==`, `!=`                 | Equals, or does not equal, the value. | `assignment.worker_type == "Contractor"`   |
| `>`, `<`, `>=`, `<=`       | Compares above or below the value.    | `assignment.start_date >= "2026-01-01"`    |
| `in`, `not in`             | Is, or is not, one of a list.         | `assignment.status in ["Active", "Leave"]` |
| `contains`                 | Contains the value.                   | `assignment.job.name contains "Engineer"`  |
| `starts with`, `ends with` | Starts or ends with the value.        | `assignment.job.code starts with "ENG"`    |

### `unmanaged`

A mapping value that tells Klef to leave a field untouched.

This is particularly useful on a grant such as `groups`, `licenses`, or `channels`. It tells Klef to retain what it had given in an earlier lifecycle stage.

```hcl theme={null}
stage active {
  target microsoft_entra.user {
    userPrincipalName = worker.business_email
    mailNickname      = worker.employee_id
    displayName       = "{{ worker.legal_name.given }} {{ worker.legal_name.family }}"
    accountEnabled    = true
    groups            = ["All Employees", "Engineering"]
  }
}

stage leave {
  target microsoft_entra.user {
    userPrincipalName = worker.business_email
    mailNickname      = worker.employee_id
    displayName       = "{{ worker.legal_name.given }} {{ worker.legal_name.family }}"
    accountEnabled    = false
    groups            = unmanaged      # <-- Keeps "All Employees" and "Engineering"
  }
}
```

### `notify`

A block that sends a message when its stage's changes apply, by email, Slack, Teams, or an HTTP request.

Its name, in quotes after the channel, is unique across your policies. Klef records what it has sent under that name, so renaming a notification makes it a new one.

```hcl theme={null}
stage active {
  notify email "accounts_ready" {  # <-- Emails each worker's manager once they are set up
    send        = once
    title       = "Accounts ready"
    aggregation = per_worker
    subject     = "{{ worker.display_name }} is set up"
    body        = "They can sign in from their first day."
    to          = [manager(1)]
  }
}
```

Every channel takes these settings:

| Setting       | Required | Description                                                                                                                                            |
| ------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `send`        | Yes      | `once` sends once per worker, the first time they reach the stage, until they are terminated. `always` sends every time they enter the stage.          |
| `title`       |          | A name for the message. Teams shows it as a heading.                                                                                                   |
| `aggregation` |          | `per_worker` sends one message for each worker, and is the default. `per_run` sends one message for the whole sync, and its text loops over `workers`. |

#### Email

| Setting   | Required | Description                                                       |
| --------- | -------- | ----------------------------------------------------------------- |
| `subject` | Yes      | The subject line.                                                 |
| `body`    | Yes      | The message.                                                      |
| `to`      | Yes      | Who receives it, as a list of recipients.                         |
| `cc`      |          | Who is copied, as a list of recipients.                           |
| `bcc`     |          | Who is copied without the others seeing, as a list of recipients. |

<Warning>
  An email can go to at most 50 addresses across `to`, `cc`, and `bcc`.
</Warning>

Each recipient is one of these:

| Recipient               | Who it is                                                                                            |
| ----------------------- | ---------------------------------------------------------------------------------------------------- |
| `worker.business_email` |                                                                                                      |
| `manager(1)`            | The worker's direct manager. `manager(2)` is their manager's manager, and so on up to `manager(10)`. |
| `"it@example.com"`      | A fixed address.                                                                                     |

```hcl theme={null}
to  = [worker.personal_email, manager(1)]
cc  = ["it@example.com"]
bcc = [member("0197c3f2-5b1a-7c3e-9d4f-2a6b8e1c0d3f")]
```

#### Slack

```hcl theme={null}
notify slack "new_starters" {
  send        = once
  title       = "New starters"
  aggregation = per_run
  text        = <<TEXT
    {% for worker in workers %}
    {{ worker.display_name }} starts as {{ worker.job.name }}.
    {% endfor %}
  TEXT
  webhook     = secret.people_channel
}
```

| Setting               | Required | Description                          |
| --------------------- | -------- | ------------------------------------ |
| `text`                | Yes      | The message.                         |
| `webhook`, `webhooks` |          | The channel(s) incoming webhook URL. |

#### Teams

```hcl theme={null}
notify teams "leaver" {
  send        = once
  title       = "Leaver"
  aggregation = per_worker
  text        = "{{ worker.display_name }} leaves on {{ worker.end_date }}."
  webhook     = secret.it_channel
}
```

| Setting               | Required | Description                          |
| --------------------- | -------- | ------------------------------------ |
| `text`                | Yes      | The message.                         |
| `webhook`, `webhooks` |          | The channel(s) incoming webhook URL. |

#### HTTP

```hcl theme={null}
notify http "joiner" {
  send        = once
  title       = "Joiner"
  aggregation = per_worker
  method      = post
  uri         = "https://hooks.example.com/joiner"
  body        = <<JSON
    { "email": "{{ worker.business_email }}", "start": "{{ worker.start_date }}" }
  JSON

  header "Authorization" = secret.hooks_token
}
```

| Setting           | Required | Description                                                     |
| ----------------- | -------- | --------------------------------------------------------------- |
| `uri`             | Yes      | Where the request goes. Must be an absolute URL.                |
| `method`          |          | `get`, `post`, `put`, `patch`, or `delete`. Defaults to `post`. |
| `body`            |          | What the request sends.                                         |
| `header "<name>"` |          | A request header.                                               |

### Passwords

If a target requires a password, Klef will set its first password. A notification can carry the link to that password as `{{ <system>.sign_in_url }}`. The password is only shown once and must be changed after first sign-in.

```hcl theme={null}
stage pre_start {
  target microsoft_entra.user {
    userPrincipalName = worker.business_email
    displayName       = worker.display_name
    accountEnabled    = true
  }

  notify email welcome {
    send    = once
    subject = "Your Microsoft account"
    body    = "Get your first sign-in here: {{ microsoft_entra.sign_in_url }}"  # <-- Links to the password
    to      = [worker.personal_email, manager(1)]
  }
}
```

### Resources

Scripts, lookup tables, and secrets are global and can be shared between policies.

```hcl theme={null}
target microsoft_entra.user {
  mailNickname  = script.mail_alias # <-- Computed by a script
  usageLocation = lookup(table.usage_locations, worker.location.country)  # <-- Translated by a lookup table
}

notify slack "access_granted" {
  send        = once
  title       = "Access granted"
  aggregation = per_worker
  text        = <<TEXT
    {{ worker.display_name }} is set up.
    Their Microsoft and Slack accounts are ready to sign in to.
    First Microsoft sign-in: {{ microsoft_entra.sign_in_url }}
  TEXT

  webhooks = [secret.engineering_channel] # <-- Read from a secret
}
```

| Resource     | Written                         | What it does                            |
| ------------ | ------------------------------- | --------------------------------------- |
| Script       | `script.<name>`                 | Computes a field's value in JavaScript. |
| Lookup table | `lookup(table.<name>, <value>)` | Maps from one value to another another. |
| Secret       | `secret.<name>`                 | Stores a credential.                    |

## An Example

```hcl theme={null}
policy "Employee lifecycle" {
  category = "Company-wide"
  applies  = everyone

  stage pre_start {
    target microsoft_entra.user {
      userPrincipalName = worker.business_email
      mailNickname      = worker.employee_id
      displayName       = "{{ worker.legal_name.given }} {{ worker.legal_name.family }}"
      givenName         = worker.legal_name.given
      surname           = worker.legal_name.family
      employeeId        = worker.employee_id
      jobTitle          = worker.job.name
      department        = worker.department.name
      usageLocation     = lookup(table.usage_locations, worker.location.country)
      manager           = worker.manager
      accountEnabled    = false
    }

    target oracle_fusion.worker {
      personNumber    = worker.employee_id
      firstName       = worker.legal_name.given
      lastName        = worker.legal_name.family
      legislationCode = "US"
      active          = false

      emails = [
        { emailType = "W1", emailAddress = worker.business_email, primaryFlag = true },
      ]

      workRelationships = [
        {
          legalEmployerName = "Acme Logistics LLC"
          workerType        = "E"
          hireDate          = worker.hire_date

          assignments = each worker.assignments
              as assignment where assignment.status in ["Pending", "Active"] {
            assignmentNumber = assignment.source_id
            assignmentName   = assignment.job.name
            actionCode       = "HIRE"
            businessUnitName = assignment.business_unit.name
            jobCode          = assignment.job.code
            locationCode     = assignment.location.code
            primaryFlag      = assignment.is_primary
            effectiveFrom    = assignment.start_date
          }
        },
      ]
    }

    notify email "new_starter" {
      send        = once
      title       = "New starter"
      aggregation = per_worker
      subject     = "{{ worker.display_name }} starts on {{ worker.start_date }}"
      body        = <<BODY
        {{ worker.display_name }} joins as {{ worker.job.name }}.
        Their Microsoft account is ready and switches on their first day.
        Hand them their first sign-in: {{ microsoft_entra.sign_in_url }}
      BODY
      to          = [manager(1)]
    }
  }

  stage active {
    target microsoft_entra.user {
      userPrincipalName = worker.business_email
      mailNickname      = worker.employee_id
      displayName       = "{{ worker.legal_name.given }} {{ worker.legal_name.family }}"
      givenName         = worker.legal_name.given
      surname           = worker.legal_name.family
      employeeId        = worker.employee_id
      jobTitle          = worker.job.name
      department        = worker.department.name
      usageLocation     = lookup(table.usage_locations, worker.location.country)
      manager           = worker.manager
      accountEnabled    = true
      licenses          = ["SPE_E3"]
      groups            = ["All Employees"]
    }

    target slack.user {
      userName    = worker.business_email
      displayName = worker.display_name
      title       = worker.job.name
      active      = true
      channels    = ["general", "announcements"]
    }

    target oracle_fusion.worker {
      personNumber    = worker.employee_id
      firstName       = worker.legal_name.given
      lastName        = worker.legal_name.family
      legislationCode = "US"
      active          = true

      emails = [
        { emailType = "W1", emailAddress = microsoft_entra.mail, primaryFlag = true },
      ]

      workRelationships = [
        {
          legalEmployerName = "Acme Logistics LLC"
          workerType        = "E"
          hireDate          = worker.hire_date

          assignments = each worker.assignments
              as assignment where assignment.status == "Active" {
            assignmentNumber = assignment.source_id
            assignmentName   = assignment.job.name
            businessUnitName = assignment.business_unit.name
            jobCode          = assignment.job.code
            locationCode     = assignment.location.code
            primaryFlag      = assignment.is_primary
            effectiveFrom    = assignment.start_date
          }
        },
      ]

      roles = ["Employee"]
    }

    notify slack "new_starters" {
      send        = once
      title       = "New starters"
      aggregation = per_run
      text        = <<TEXT
        {% for worker in workers %}
        Welcome {{ worker.display_name }}, who joins as {{ worker.job.name }}.
        {% endfor %}
      TEXT
      webhook     = secret.announcements_channel
    }
  }

  stage departing {
    notify email "leaver_handover" {
      send        = once
      title       = "Leaver"
      aggregation = per_worker
      subject     = "{{ worker.display_name }} leaves on {{ worker.end_date }}"
      body        = "Their access closes at the end of their last day. Please plan the handover."
      to          = [manager(1)]
      cc          = ["it@example.com"]
    }
  }

  stage terminated {
    target microsoft_entra.user {
      userPrincipalName = worker.business_email
      mailNickname      = worker.employee_id
      displayName       = "{{ worker.legal_name.given }} {{ worker.legal_name.family }}"
      accountEnabled    = false
      licenses          = []
      groups            = []
    }

    target slack.user {
      userName = worker.business_email
      active   = false
    }

    target oracle_fusion.worker {
      personNumber    = worker.employee_id
      firstName       = worker.legal_name.given
      lastName        = worker.legal_name.family
      legislationCode = "US"
      active          = false
      roles           = []
    }

    notify http "leaver" {
      send        = once
      title       = "Leaver"
      aggregation = per_worker
      method      = post
      uri         = "https://hooks.example.com/leavers"
      body        = <<JSON
        { "employeeId": "{{ worker.employee_id }}", "lastDay": "{{ worker.end_date }}" }
      JSON

      header "Authorization" = secret.hooks_token
    }
  }
}
```
