# Appfolio Core Workflow A

> Build property management dashboard with AppFolio API data. Trigger: "appfolio property dashboard".

- Skill: `gabrielmoreira/appfolio-core-workflow-a` (Agent Skill)
- Install (CLI): `npx skillmds@latest add gabrielmoreira/appfolio-core-workflow-a`
- Raw SKILL.md: https://api.skillmd.com/api/skills/gabrielmoreira/appfolio-core-workflow-a/raw
- Safety review: pending (external: skill-scanner PASS, skillspector PASS)
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Integrations & APIs
- License: MIT
- Author: gabrielmoreira (https://skillmd.com/u/gabrielmoreira)
- Updated: 2026-09-09
- Page: https://skillmd.com/skills/gabrielmoreira/appfolio-core-workflow-a

---

# AppFolio — Property & Tenant Management

## Overview

Primary workflow for AppFolio property management integration. Covers the full property
lifecycle: creating and updating property records, managing tenant profiles and lease
agreements, and querying occupancy data. Uses the AppFolio Stack API with OAuth 2.0
client credentials for server-to-server access. All endpoints return JSON and support
pagination via `cursor` parameters for large portfolios.

## Prerequisites

- A current AppFolio partner/API contract that confirms the assigned portfolio
  base URL, authentication method, approved scopes, and write capabilities;
  do not infer these from a tutorial or another portfolio.
- A separate sandbox with disposable properties, units, and synthetic tenant
  records, plus a credential owner and an approved request budget.
- Idempotency keys and an operator-approved rollback/reconciliation process for
  property, tenant, and lease writes, which can create real legal and billing
  consequences.

## Instructions

1. Confirm the provider-issued base URL and authentication flow for the target
   portfolio before copying any client configuration into an environment.
2. Exercise the create/update path only with sandbox synthetic fixtures and
   idempotency keys; retrieve canonical records before retrying an uncertain
   write.
3. Validate ownership, lease dates, and unit availability before creating a
   lease, and emit only record IDs and redacted status evidence.
4. Promote a write-capable flow only after a staged rehearsal proves rollback,
   reconciliation, and the least-privilege scope boundary.

### Step 1: Authenticate and Initialize Client

```typescript
const token = await fetch('https://api.appfolio.com/oauth/token', {
  method: 'POST',
  headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
  body: new URLSearchParams({
    grant_type: 'client_credentials',
    client_id: process.env.APPFOLIO_CLIENT_ID!,
    client_secret: process.env.APPFOLIO_CLIENT_SECRET!,
    scope: 'properties tenants leases',
  }),
}).then(r => r.json());
const headers = { Authorization: `Bearer ${token.access_token}`, 'Content-Type': 'application/json' };
```

### Step 2: Create or Update a Property

```typescript
const property = await fetch('https://api.appfolio.com/v1/properties', {
  method: 'POST', headers,
  body: JSON.stringify({
    name: 'Sunrise Apartments',
    address: { street: '100 Main St', city: 'Austin', state: 'TX', zip: '78701' },
    type: 'residential',
    units: [
      { number: '101', bedrooms: 2, bathrooms: 1, rent: 1450 },
      { number: '102', bedrooms: 1, bathrooms: 1, rent: 1100 },
    ],
  }),
}).then(r => r.json());
console.log(`Property created: ${property.id}`);
```

### Step 3: Add a Tenant and Lease

```typescript
const tenant = await fetch('https://api.appfolio.com/v1/tenants', {
  method: 'POST', headers,
  body: JSON.stringify({
    first_name: 'Test', last_name: 'Tenant',
    email: 'test-tenant@example.invalid', phone: '555-0100',
  }),
}).then(r => r.json());

await fetch('https://api.appfolio.com/v1/leases', {
  method: 'POST', headers,
  body: JSON.stringify({
    property_id: property.id, unit_number: '101',
    tenant_id: tenant.id, start_date: '2026-05-01', end_date: '2027-04-30',
    monthly_rent: 1450, security_deposit: 1450,
  }),
}).then(r => r.json());
```

### Step 4: Query Occupancy

```typescript
const units = await fetch(
  `https://api.appfolio.com/v1/properties/${property.id}/units?status=vacant`,
  { headers },
).then(r => r.json());
console.log(`Vacant units: ${units.data.length} of ${units.meta.total}`);
```

## Error Handling

| Issue | Cause | Fix |
|-------|-------|-----|
| `401 Unauthorized` | Expired or invalid token | Re-authenticate with client credentials |
| `404 Not Found` | Wrong property/tenant ID | Verify resource ID exists before referencing |
| `422 Unprocessable` | Missing required fields | Check `errors[]` array in response body |
| `409 Conflict` | Duplicate lease for unit | Query existing leases before creating |
| `429 Too Many Requests` | Rate limit exceeded | Back off using `Retry-After` header value |

## Output

A successful run creates a property with units, adds a tenant, binds them via a lease,
and reports vacancy counts. Console output confirms each resource ID on creation.

## Examples

For a lease-creation change, run against a named synthetic sandbox unit and
tenant with a stable idempotency key. Read the property and current leases,
perform the candidate write once, then read back the resulting lease and record
only IDs, dates, and the reconciliation status. Re-run the same request to
prove it does not create a second lease. If the provider contract, endpoint,
authorization, or prior-write outcome cannot be verified, stop before any
write and send the case to the portfolio operator for reconciliation.

## Resources

- [AppFolio Stack APIs](https://www.appfolio.com/stack/partners/api)
- [AppFolio Engineering Blog](https://engineering.appfolio.com)

## Next Steps

Continue with `appfolio-core-workflow-b` for maintenance requests and payment tracking.

