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

# Auto-Enroll a New Hire

> Detect a new employee and start their benefits enrollment without acting on the entire existing workforce.

## What you'll use

**Models** - employee, employment, employer benefit, benefit
**Events** - `employee_data_changed`, `connector_synced`

<Info>
  **Before you start**

  * A webhook is registered for the environment - see [Create a Webhook](/guides/reading-writing/webhooks).
  * You can persist a set of employee IDs per connector. Without that, you cannot tell a new hire from an update.
</Info>

## Steps

<Steps>
  <Step title="Subscribe to employee changes">
    Subscribe to `employee_data_changed`, and to `connector_synced` if you need to know the run finished before acting - see [Choose Webhook Events](/guides/reading-writing/webhooks).

    The change event carries IDs, not records:

    ```json theme={null}
    {
      "webhook": { "event": "employee_data_changed" },
      "connector": { "origin_id": "tenant_9f2c1b40", "integration_slug": "bamboohr" },
      "data": ["0190d9d8-ac9b-76cb-a5ee-df70994e6f3d"],
      "sync": { "sync_id": "019f146e-...", "sync_type": "AUTOMATED", "sync_name": "#13" }
    }
    ```

    **Result:** A list of employee IDs that changed, and the connector they belong to.
  </Step>

  <Step title="Guard against the first sync">
    On a connector's first run every employee is new to you, and processing them as hires is not recoverable. Check before doing anything else.

    ```python theme={null}
    if sync["sync_name"] == "#1" or connector_id not in self.baselined:
        store_baseline(connector_id, event["data"])   # record IDs, run no workflows
        return
    ```

    **Result:** Existing employees establish baseline state instead of triggering enrollment.
  </Step>

  <Step title="Separate creations from updates">
    Diff the event's IDs against the ones you've already seen for this connector. Nothing in the payload distinguishes the two - see [Created vs modified](/guides/reading-writing/webhooks).

    ```python theme={null}
    new_ids = set(event["data"]) - seen_ids[connector_id]
    if not new_ids:
        return
    ```

    Key on the Bindbee `id`. `remote_id` and `employee_number` can be reissued upstream, which makes an update look like a hire.

    **Result:** Only genuinely new employee records.
  </Step>

  <Step title="Fetch those employees with their employment">
    Request the IDs directly rather than paging the collection. `start_date` and `employment_status` are both on the employee, so one call is enough.

    ```bash theme={null}
    curl --request GET \
      --url 'https://api.bindbee.dev/api/hris/v1/employees?ids=<ID_1>,<ID_2>' \
      --header 'Authorization: Bearer <BINDBEE_API_KEY>' \
      --header 'X-Connector-Token: <CONNECTOR_TOKEN>'
    ```

    **Result:** Full records for the new employees, including their start dates.
  </Step>

  <Step title="Gate on start date, not on arrival">
    A record appearing is not a hire happening. Future-dated hires arrive weeks early and carry `PENDING`.

    ```python theme={null}
    for emp in employees:
        if emp["employment_status"] == "PENDING":
            schedule_for(emp, emp["start_date"])   # don't act yet
        elif emp["employment_status"] == "ACTIVE":
            begin_enrollment(emp)
    ```

    Treat the `PENDING` to `ACTIVE` transition as its own signal - offers are withdrawn, and a workflow started on arrival reaches people who never join.

    **Result:** Enrollment starts when employment does.
  </Step>

  <Step title="Read the plans available to them">
    Fetch the employer's plan catalog, which is what the employee will be choosing from.

    ```bash theme={null}
    curl --request GET \
      --url 'https://api.bindbee.dev/api/hris/v1/employer-benefits?page_size=200' \
      --header 'Authorization: Bearer <BINDBEE_API_KEY>' \
      --header 'X-Connector-Token: <CONNECTOR_TOKEN>'
    ```

    This is the plan definition, not anyone's enrollment - see [Reading benefits](/guides/data-models/benefits). Which of these plans a given employee is eligible for is not readable; see the constraint below.

    **Result:** The plans to present, per customer.
  </Step>
</Steps>

## Constraints specific to this combination

**The first sync will look like everyone was hired at once.** When a connector is first connected, every employee is new to you. Without a guard, this triggers enrollment workflows for the entire workforce - the failure mode is a welcome email to several thousand existing employees, and it is not recoverable.

Handle the initial sync as a bulk import that establishes baseline state without triggering per-employee workflows, then switch to event-driven handling. `sync.sync_name` increments per run, which is one way to recognize the first. The same applies after a [forced resync](/guides/reading-writing/syncing).

**New in Bindbee is not the same as newly hired.** A record can appear because the employee was just hired, because they came into scope after a permission or configuration change, or because a prior sync failed to load them. Check the start date rather than treating first-appearance as a hire.

**Pre-start records arrive before the person does.** Future-dated hires are commonly entered weeks in advance and carry `PENDING`. That's useful - it's the window in which enrollment paperwork should happen - but it means acting on arrival can start a workflow for someone who hasn't joined and may never join. Offers are withdrawn. Gate on start date, and handle the `PENDING` to `ACTIVE` transition as a separate signal.

**Eligibility isn't in the employee model.** Waiting periods, hours thresholds, and class-based eligibility rules live in the customer's benefits configuration, not in anything you can read. You can read the start date and employment type; you cannot read "eligible on 1 October". Agree the rules per customer and compute eligibility yourself.

**The window is short and sync latency eats it.** Enrollment windows typically run 30 days from hire, and a 24-hour cadence plus your own processing interval consumes a meaningful share before you begin. Measure the gap between hire date and first contact for a real customer before assuming the schedule is adequate.

## Frequently Asked Questions

<AccordionGroup>
  <Accordion title="Every employee arrived as a new hire">
    A first sync or a resync. Both re-sync the whole population, so every record looks changed - see [modified\_after](/guides/reading-writing/reading-data/modified-after) for why. This is what the baseline guard in step 2 exists to prevent.
  </Accordion>

  <Accordion title="No event fired though a hire was entered">
    Change events fire only when a sync actually changes records, and a sync only picks up the hire on its next run. On a Development connector there is no schedule at all - see [Development connectors](/guides/reading-writing/syncing).
  </Accordion>

  <Accordion title="A start date is missing or in the past">
    Backdated entries are routine for hires processed late, and `start_date` is null on some integrations. Where it is, the employment's `effective_date` is the fallback - see [Reading employees](/guides/data-models/employee-data).
  </Accordion>

  <Accordion title="An enrollment started for someone who never joined">
    A `PENDING` record acted on at arrival rather than at start date. Withdrawn offers and deferred start dates both produce this.
  </Accordion>
</AccordionGroup>

## Related

* [Created vs modified](/guides/reading-writing/webhooks)
* [Reading employees](/guides/data-models/employee-data)
* [Reading benefits](/guides/data-models/benefits)
