---
llm-context: developer-guide
category: workflows
title: Employee Offboarding Workflow
difficulty: beginner
time: 5 minutes
format: markdown
updated: 
---


# Employee Offboarding Workflow

Complete code to revoke employee access when they leave. Fast, secure, and maintains full audit trail.

## The Workflow

**Scenario:** Employee&#39;s last day is Friday. Revoke all building access immediately.

**What we&#39;ll do:**
1. Disable person record (fastest method)
2. Verify access is revoked
3. Maintain audit trail

**Time:** About 1 second per employee

**Alternative:** Disable credentials individually (slower, more granular)

## Why Disable (Not Delete)?

**Disabling:**
- Keeps audit trail intact
- Can see historical access events
- Can re-enable if needed
- Maintains compliance records

**Deleting:**
- Removes from audit trail
- Can&#39;t see historical access
- Permanent action
- Use only for data retention compliance

**Recommended:** Always disable, never delete.

## Quick Offboarding (Disable Person)

Fastest method - disables person and all their credentials:

### Bash Script

```bash
#!/bin/bash
# Employee Offboarding Script
# Usage: ./offboard_employee.sh 12345

PERSON_ID=$1
ACCESS_TOKEN=&quot;your_access_token_here&quot;
BASE_URL=&quot;https://api.doorflow.com/api/3&quot;

echo &quot;Offboarding person ID: $PERSON_ID...&quot;

# Disable person (disables all credentials automatically)
curl -s -X PUT &quot;$BASE_URL/people/$PERSON_ID&quot; \
  -H &quot;Authorization: Bearer $ACCESS_TOKEN&quot; \
  -H &quot;Content-Type: application/json&quot; \
  -d &#39;{
    &quot;enabled&quot;: false
  }&#39; | jq &#39;.&#39;

echo &quot;[OK] Person disabled&quot;
echo &quot;[OK] All access revoked&quot;
echo &quot;&quot;
echo &quot;Person record retained for audit trail.&quot;
echo &quot;Physical card should be collected during exit interview.&quot;
```

**Save as:** `offboard_employee.sh`

**Run:**
```bash
chmod +x offboard_employee.sh
./offboard_employee.sh 12345
```

### JavaScript Version

```javascript
async function offboardEmployee(personId) {
  const accessToken = process.env.DOORFLOW_ACCESS_TOKEN;
  const baseURL = &#39;https://api.doorflow.com/api/3&#39;;

  try {
    console.log(`Offboarding person ID: ${personId}...`);

    // Disable person
    const response = await fetch(`${baseURL}/people/${personId}`, {
      method: &#39;PUT&#39;,
      headers: {
        &#39;Authorization&#39;: `Bearer ${accessToken}`,
        &#39;Content-Type&#39;: &#39;application/json&#39;,
      },
      body: JSON.stringify({
        enabled: false,
      }),
    });

    if (!response.ok) {
      const error = await response.json();
      throw new Error(`Failed to offboard: ${error.error || error.message}`);
    }

    const person = await response.json();
    console.log(`[OK] Person disabled: ${person.first_name} ${person.last_name}`);
    console.log(&#39;[OK] All access revoked&#39;);

    return {
      personId: person.id,
      firstName: person.first_name,
      lastName: person.last_name,
      offboardedAt: new Date(),
    };

  } catch (error) {
    console.error(&#39;Offboarding failed:&#39;, error);
    throw error;
  }
}

// Usage
offboardEmployee(12345)
  .then(result =&gt; {
    console.log(&#39;Offboarding complete:&#39;, result);
    console.log(&#39;\nNext steps:&#39;);
    console.log(&#39;1. Collect physical access card&#39;);
    console.log(&#39;2. Update HR system&#39;);
    console.log(&#39;3. Verify access revoked at a door&#39;);
  })
  .catch(error =&gt; console.error(&#39;Error:&#39;, error.message));
```

## Alternative: Disable Credentials Individually

More granular approach - disable each credential separately:

```javascript
async function offboardEmployeeDetailed(personId) {
  const accessToken = process.env.DOORFLOW_ACCESS_TOKEN;
  const baseURL = &#39;https://api.doorflow.com/api/3&#39;;

  try {
    // Step 1: Get all credentials
    console.log(&#39;Retrieving credentials...&#39;);

    const credResponse = await fetch(`${baseURL}/people/${personId}/credentials`, {
      headers: {
        &#39;Authorization&#39;: `Bearer ${accessToken}`,
      },
    });

    if (!credResponse.ok) {
      throw new Error(`Failed to get credentials: ${await credResponse.text()}`);
    }

    const { credentials } = await credResponse.json();
    console.log(`Found ${credentials.length} credentials`);

    // Step 2: Disable each credential
    for (const credential of credentials) {
      await fetch(`${baseURL}/people/${personId}/credentials/${credential.id}`, {
        method: &#39;PUT&#39;,
        headers: {
          &#39;Authorization&#39;: `Bearer ${accessToken}`,
          &#39;Content-Type&#39;: &#39;application/json&#39;,
        },
        body: JSON.stringify({
          person_credential: {
            enabled: false,
          },
        }),
      });

      console.log(`[OK] Disabled ${credential.credential_type_name} (${credential.value})`);
    }

    // Step 3: Remove from all groups (optional)
    await fetch(`${baseURL}/people/${personId}`, {
      method: &#39;PUT&#39;,
      headers: {
        &#39;Authorization&#39;: `Bearer ${accessToken}`,
        &#39;Content-Type&#39;: &#39;application/json&#39;,
      },
      body: JSON.stringify({
        group_ids: [], // Remove from all groups
      }),
    });

    console.log(&#39;[OK] Removed from all groups&#39;);
    console.log(&#39;[OK] Offboarding complete&#39;);

    return {
      personId,
      credentialsDisabled: credentials.length,
      offboardedAt: new Date(),
    };

  } catch (error) {
    console.error(&#39;Offboarding failed:&#39;, error);
    throw error;
  }
}

// Usage
offboardEmployeeDetailed(12345)
  .then(result =&gt; console.log(&#39;Detailed offboarding complete:&#39;, result));
```

**When to use detailed method:**
- Need to log exactly which credentials were disabled
- Want to disable only certain credentials (not all)
- Maintaining separate audit trail per credential

## Bulk Offboarding

Offboard multiple employees at once:

```javascript
async function bulkOffboardEmployees(personIds) {
  console.log(`Offboarding ${personIds.length} employees...`);

  const results = [];

  for (const personId of personIds) {
    try {
      const result = await offboardEmployee(personId);
      results.push({ success: true, ...result });

      // Small delay to avoid rate limits
      await new Promise(resolve =&gt; setTimeout(resolve, 1000));

    } catch (error) {
      console.error(`Failed to offboard person ${personId}:`, error.message);
      results.push({
        success: false,
        personId: personId,
        error: error.message,
      });
    }
  }

  // Summary
  const successful = results.filter(r =&gt; r.success);
  const failed = results.filter(r =&gt; !r.success);

  console.log(`\n[OK] Bulk offboarding complete`);
  console.log(`Successful: ${successful.length}`);
  console.log(`Failed: ${failed.length}`);

  return results;
}

// Usage
const employeesToOffboard = [12345, 12346, 12347];
bulkOffboardEmployees(employeesToOffboard)
  .then(results =&gt; {
    // Log results
    console.log(&#39;Results:&#39;, results);
  });
```

## Scheduled Offboarding

Automatically offboard employee on their last day:

```javascript
async function scheduleOffboarding(personId, lastDay) {
  // Store in database for cron job to process
  await db.insert(&#39;scheduled_offboarding&#39;, {
    person_id: personId,
    offboard_date: lastDay,
    status: &#39;pending&#39;,
  });

  console.log(`Offboarding scheduled for person ${personId} on ${lastDay}`);
}

// Cron job runs daily at 5 PM
async function processScheduledOffboarding() {
  const today = new Date().toISOString().split(&#39;T&#39;)[0];

  // Get employees scheduled for offboarding today
  const scheduled = await db.query(
    &#39;SELECT person_id FROM scheduled_offboarding WHERE offboard_date = ? AND status = ?&#39;,
    [today, &#39;pending&#39;]
  );

  console.log(`Processing ${scheduled.length} scheduled offboardings...`);

  for (const row of scheduled) {
    try {
      await offboardEmployee(row.person_id);

      // Mark as complete
      await db.update(&#39;scheduled_offboarding&#39;, {
        status: &#39;completed&#39;,
        completed_at: new Date(),
      }, { person_id: row.person_id });

    } catch (error) {
      console.error(`Failed to offboard ${row.person_id}:`, error.message);

      // Mark as failed
      await db.update(&#39;scheduled_offboarding&#39;, {
        status: &#39;failed&#39;,
        error: error.message,
      }, { person_id: row.person_id });
    }
  }
}

// Usage: Schedule offboarding
const lastDay = new Date(&#39;2024-06-30&#39;);
scheduleOffboarding(12345, lastDay);

// Usage: Process scheduled (run via cron daily)
// 0 17 * * * node process_offboarding.js
processScheduledOffboarding();
```

## Verify Access Revoked

Always verify offboarding succeeded:

```javascript
async function verifyOffboarding(personId) {
  const accessToken = process.env.DOORFLOW_ACCESS_TOKEN;
  const baseURL = &#39;https://api.doorflow.com/api/3&#39;;

  // Check person is disabled
  const personResponse = await fetch(`${baseURL}/people/${personId}`, {
    headers: { &#39;Authorization&#39;: `Bearer ${accessToken}` },
  });

  const person = await personResponse.json();

  if (person.enabled === false) {
    console.log(`[OK] Person is disabled`);
  } else {
    console.log(`[WARNING] Person is still enabled!`);
    return false;
  }

  // Check credentials are disabled
  const credResponse = await fetch(`${baseURL}/people/${personId}/credentials`, {
    headers: { &#39;Authorization&#39;: `Bearer ${accessToken}` },
  });

  const { credentials } = await credResponse.json();
  const enabledCredentials = credentials.filter(c =&gt; c.enabled);

  if (enabledCredentials.length === 0) {
    console.log(`[OK] All credentials disabled`);
  } else {
    console.log(`[WARNING] ${enabledCredentials.length} credentials still enabled!`);
    return false;
  }

  return true;
}

// Usage
offboardEmployee(12345)
  .then(() =&gt; verifyOffboarding(12345))
  .then(verified =&gt; {
    if (verified) {
      console.log(&#39;[OK] Offboarding verified&#39;);
    } else {
      console.log(&#39;[ERROR] Offboarding verification failed&#39;);
    }
  });
```

## Integration with HR System

Automatically offboard when HR system updates:

```javascript
// When employee marked as terminated in HR system
hrSystem.on(&#39;employee.terminated&#39;, async (employee) =&gt; {
  try {
    // Find person in DoorFlow by email
    const searchResponse = await fetch(
      `${baseURL}/people?email=${employee.workEmail}`,
      {
        headers: { &#39;Authorization&#39;: `Bearer ${accessToken}` },
      }
    );

    const { people } = await searchResponse.json();

    if (people.length === 0) {
      console.log(`No DoorFlow person found for ${employee.workEmail}`);
      return;
    }

    const person = people[0];

    // Offboard in DoorFlow
    await offboardEmployee(person.id);

    // Log in HR system
    await hrSystem.addNote(
      employee.id,
      `Building access revoked in DoorFlow on ${new Date().toISOString()}`
    );

    // Notify facilities to collect card
    await emailService.send({
      to: &#39;facilities@company.com&#39;,
      subject: `Collect access card from ${employee.firstName} ${employee.lastName}`,
      body: `Employee&#39;s last day is ${employee.lastDay}. Please collect building access card.`,
    });

    console.log(`[OK] ${employee.firstName} ${employee.lastName} offboarded`);

  } catch (error) {
    // Notify HR of failure
    await hrSystem.addNote(
      employee.id,
      `DoorFlow offboarding failed: ${error.message}. Manual action required.`
    );
  }
});
```

## Best Practices

**1. Offboard immediately on last day:**
- Don&#39;t wait weeks after employee leaves
- Schedule for 5 PM on last working day
- Revoke before collecting card (prevents unauthorized use)

**2. Collect physical cards:**
- Include in exit interview checklist
- Track card collection in your system
- Return card to inventory for reuse

**3. Maintain audit trail:**
- Never delete person records
- Always disable, not delete
- Log offboarding action in your system

**4. Verify offboarding:**
```javascript
async function offboardWithVerification(personId) {
  await offboardEmployee(personId);
  const verified = await verifyOffboarding(personId);

  if (!verified) {
    throw new Error(&#39;Offboarding verification failed - manual review required&#39;);
  }

  return true;
}
```

**5. Handle errors gracefully:**
```javascript
try {
  await offboardEmployee(personId);
} catch (error) {
  // Log error
  console.error(&#39;Offboarding error:&#39;, error);

  // Notify security team
  await notifySecurityTeam(`Manual offboarding required for person ${personId}: ${error.message}`);

  // Create manual task
  await createManualTask({
    type: &#39;offboarding&#39;,
    personId: personId,
    priority: &#39;high&#39;,
    reason: error.message,
  });
}
```

## Common Errors

**&quot;Person not found&quot;**
- Person ID doesn&#39;t exist
- Double-check person ID
- Search by email: `GET /api/3/people?email=user@example.com`

**&quot;Insufficient permissions&quot;**
- OAuth scope `account.person` required
- Check your access token scopes

**&quot;Person already disabled&quot;**
- Person was already offboarded
- Verify in DoorFlow UI or via API

## Offboarding Checklist

**Before offboarding:**
- [ ] Verify person ID is correct
- [ ] Confirm this is the employee&#39;s last day
- [ ] Check for scheduled offboarding (avoid duplicates)

**During offboarding:**
- [ ] Disable person record
- [ ] Verify offboarding succeeded
- [ ] Log action in your system

**After offboarding:**
- [ ] Collect physical access card
- [ ] Return card to inventory
- [ ] Update HR system
- [ ] Verify no access attempts in events log

## Required OAuth Scopes

- `account.person` - Update person record to disable
- `account.person` (optional) - If disabling credentials individually

## Next Steps

**Related workflows:**
- [Employee Onboarding] - Onboard new employees
- [Contractor Access] - Time-limited access revocation
- [Events and Audit Trail] - Monitor offboarded employee access attempts

**Learn more:**
- [Core Resources] - Understanding people and credentials
- [Access Control Model] - How permissions work

