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

# Automations – Complete Guide

> Everything you can build with Kardow Automations: triggers, actions, integrations, and what to plan for next.

Kardow Automations is a built-in workflow engine that lets you react to anything that happens on your platform and chain any number of follow-up actions — without writing custom server code for each use case.

Think of it as the backbone that connects your job board to the rest of your stack: email sequences, Slack alerts, CRM syncs, subscription access on third-party platforms, and custom Cloudflare Workers you build yourself.

***

## The core idea

Every automation has two parts:

1. **A trigger** — an event that happens in your platform (a new job is posted, a user signs up, a payment succeeds, etc.)
2. **One or more actions** — what happens in response (send an email, POST to a webhook, wait 24 hours, call a Cloudflare Worker, etc.)

You wire them together in the **Automations** section of your dashboard. Once active, they run automatically in the background for every matching event.

***

## All available triggers

Triggers are grouped by area of your platform.

### Jobs

| Trigger               | When it fires                               |
| --------------------- | ------------------------------------------- |
| `job.created`         | Any new job is posted (catches all sources) |
| `job.created.user`    | A user posts a job directly                 |
| `job.created.scraper` | The scraper imports a new job               |
| `job.created.admin`   | An admin creates a job from the dashboard   |
| `job.updated`         | Any job is edited                           |
| `job.approved`        | A pending job is approved                   |
| `job.expired`         | A job reaches its expiry date               |
| `job.deleted`         | A job is deleted                            |

**Tip**: Use `job.created` when you want a single automation for all new jobs. Use `job.created.scraper` if you want scraper imports handled separately (e.g., no notification email for automated imports).

### Users

| Trigger                  | When it fires              |
| ------------------------ | -------------------------- |
| `user.created`           | Any new account is created |
| `user.created.employer`  | A new employer registers   |
| `user.created.candidate` | A new candidate registers  |
| `user.created.admin`     | A new admin is created     |
| `user.updated`           | A user profile is updated  |

### Applications

| Trigger                      | When it fires                                                       |
| ---------------------------- | ------------------------------------------------------------------- |
| `application.created`        | Someone applies to a job                                            |
| `application.updated`        | An application is edited                                            |
| `application.status_changed` | An application moves to a new status (reviewed, accepted, rejected) |

### Payments & Subscriptions

| Trigger                  | When it fires                                        |
| ------------------------ | ---------------------------------------------------- |
| `subscription.created`   | A new subscription starts                            |
| `subscription.cancelled` | A subscription is cancelled                          |
| `subscription.updated`   | A subscription changes (plan upgrade, renewal, etc.) |
| `payment.succeeded`      | A payment goes through successfully                  |
| `payment.failed`         | A payment fails                                      |
| `invoice.created`        | An invoice is generated                              |

### Custom / External

| Trigger            | When it fires                                                  |
| ------------------ | -------------------------------------------------------------- |
| `webhook.received` | An inbound webhook payload arrives at your automation endpoint |
| `schedule.cron`    | A recurring schedule you define                                |

***

## All available actions

Each step in your workflow is one of these action types. You can chain as many as you need.

### Send Email

Send a transactional email using your configured email provider. Supports template variables from the trigger payload (e.g. `{{title}}`, `{{email}}`, `{{company_name}}`).

**Config fields**: To, Subject, Body (HTML)

**Example uses**:

* Welcome email when a new employer registers
* "Your job has been approved" notification
* Re-engagement email 3 days after a subscription lapses

***

### Send Webhook

POST the trigger payload + your custom data to any external URL.

**Config fields**: URL, HTTP method

**Example uses**:

* Sync new users to a CRM (HubSpot, Pipedrive)
* Notify a Slack channel when a premium job is posted
* Send subscription events to a billing dashboard

***

### Wait / Delay

Pause the workflow for a set number of seconds before continuing. Powered by QStash so the delay survives server restarts — you don't need to keep a process running.

**Config fields**: Seconds

**Example uses**:

* Wait 3 days after signup before sending a "how's it going?" email
* Wait 1 hour after job approval before tweeting about it
* Buffer between sequential webhook calls to avoid rate limits

***

### HTTP Request

Make a fully custom HTTP call — any method, any URL, custom headers.

**Config fields**: URL, Method (GET/POST/PUT/PATCH/DELETE), Headers, Body

**Example uses**:

* Call a Circle.so API to grant or revoke community membership
* Hit a third-party API to post on social platforms
* Trigger a GitHub Actions workflow

***

### Send Notification

Send a formatted message to a Discord or Slack channel via webhook.

**Config fields**: Webhook URL, Provider (discord/slack), Message, Title

**Example uses**:

* "#new-signups" channel alert with employer details
* "#payments" alert when a subscription is cancelled
* "#jobs" alert when a scraped job is imported

***

### Run Worker

Call a Cloudflare Worker you built yourself. The worker receives the automation context and can do anything — custom logic, database writes, third-party API calls.

**Config fields**: Worker URL

**Example uses**:

* AI-generated social post about a new job
* Custom scoring/matching logic for applicants
* Write to a database outside Supabase
* Any future action type you want to add

***

### Condition

Branch the workflow based on a field in the trigger payload. If the condition is not met, the rest of the workflow stops.

**Config fields**: Field, Operator (equals / not\_equals / contains / exists / greater\_than / less\_than / after\_date / before\_date), Value

**Example uses**:

* Only send a Discord alert if `job.salary_type` equals `paid`
* Only grant Circle access if `subscription.plan_type` equals `premium`
* Only send a re-engagement email if `user.email` exists
* Only import jobs posted after a specific date

***

### Update Record / Create Record

Write directly to your Supabase database as an automation step.

**Config fields**: Table, Data (key-value pairs), Match column + value (for updates)

**Example uses**:

* Automatically set `user.approved = true` for users who pay
* Create a `notifications` row when a new application arrives
* Update a job's status when its subscription expires

***

### Fetch Data

Pull data from any external JSON API, RSS feed, or XML endpoint. The fetched items are stored in the pipeline for downstream actions (Filter, Loop, Create Record).

**Config fields**: URL, Method (GET/POST), Format (json / xml / rss), Data Path (for JSON — the dot-notation path to the items array), Headers

**Example uses**:

* Pull job listings from an XML feed daily
* Fetch new postings from a partner API
* Import data from any REST endpoint

***

### Filter / Transform

Filter fetched items by rules and map external field names to your internal data model. Lets you control which items make it through and how they map to Kardow fields.

**Config fields**: Filter rules (field + operator + value), Field mapping (10 job fields: title, description, company\_name, location, job\_type, salary, how\_to\_apply, url, category, tags), Assign To (user ID), Only Import New (with date field)

**Operators**: equals, not\_equals, contains, not\_contains, exists, after\_date, before\_date

**Example uses**:

* Only import jobs from today using the `after_date` operator on `pubDate`
* Map `jobTitle` from an external feed to `title` in your job board
* Assign all imported jobs to a specific user account
* Filter out part-time jobs by checking `job_type` not\_equals `part-time`

***

### For Each Item (Loop)

Iterate over fetched items and run all subsequent actions once per item. Each item's fields become available as `{{field_name}}` template variables in downstream actions.

**Config fields**: None — it automatically iterates over the items from Fetch Data / Filter.

**Example uses**:

* Create a job record for each item in an imported feed
* Send a notification for each new posting
* Process each row from an API response individually

***

## Template variables

Any text field in an action config supports `{{variable}}` syntax. Variables come from the trigger payload. Nested access works with dot notation: `{{user.email}}`, `{{job.title}}`.

```
Subject: Your job "{{title}}" is now live on {{company_name}}
To: {{email}}
```

***

## Integration recipes

### Circle.so — grant/revoke community access automatically

When a user subscribes, use the **HTTP Request** action to call the Circle.so API and add them to your community. When they cancel, revoke access.

```
Trigger: subscription.created
Action 1 (HTTP Request):
  URL: https://app.circle.so/api/v1/community_members
  Method: POST
  Body: { "email": "{{user_email}}", "community_id": "YOUR_ID" }
```

For cancellations:

```
Trigger: subscription.cancelled
Action 1 (HTTP Request):
  URL: https://app.circle.so/api/v1/community_members/remove
  Method: POST
  Body: { "email": "{{user_email}}", "community_id": "YOUR_ID" }
```

**Effort**: 10 minutes. You only need a Circle.so API key and your community ID.

***

### Social platforms — post about new jobs

Use the **Run Worker** action with a Cloudflare Worker that accepts a job payload and posts to whatever platform you target (Twitter/X via their API, LinkedIn, etc.).

**Effort**: Moderate. You need:

* A Cloudflare Worker (50–100 lines of JavaScript)
* API credentials for each platform
* The platform's OAuth app approved (LinkedIn takes a few days)

**What the system provides**: The trigger fires automatically on every `job.created`, the worker URL is configurable per automation, and you can add a **Condition** step to only post jobs above a certain salary or from certain categories.

***

### Stripe / payment sync

Automations already fire on `subscription.created`, `subscription.cancelled`, and `payment.succeeded` from Stripe webhooks. Use the **Send Webhook** action to forward these events to any third-party tool in real time.

For bidirectional control — e.g., cancelling a subscription from an external tool — use the external API keys system (`/api/external`) with the subscriptions endpoint.

***

### HubSpot / CRM sync

```
Trigger: user.created.employer
Action 1 (Send Webhook):
  URL: https://api.hubapi.com/contacts/v1/contact/createOrUpdate/email/{{email}}
  Method: POST
  Body: { "properties": [{ "property": "email", "value": "{{email}}" }] }
```

Add your HubSpot private app token in the Headers field.

***

## Scheduled data imports

The data import pipeline lets you automatically pull jobs (or any data) from external sources on a schedule.

### How it works

A scheduled import automation uses four actions in sequence:

```
Trigger: schedule.cron (e.g. daily at 6 AM UTC)
→ Fetch Data: GET https://your-api.com/jobs.json (or RSS/XML feed)
→ Filter / Transform: only today's items, map fields to Kardow schema
→ For Each Item: iterate over filtered results
→ Create Record: insert each item into the jobs table
```

### Step-by-step setup

1. Go to **Automations** in your dashboard
2. Select **Scheduled Job Import** from the Scheduled category
3. Set the trigger interval (e.g. Daily at 6 AM UTC)
4. In **Fetch Data**, paste your source URL and select the data format:
   * **JSON**: Set the data path to the array key (e.g. `results.jobs`)
   * **RSS/XML**: Items are extracted automatically from `<item>` or `<entry>` tags
5. In **Filter / Transform**:
   * Add filter rules (e.g. `status` equals `active`, `pubDate` after\_date `today`)
   * Map your external fields to Kardow fields (e.g. `jobTitle` → `title`, `companyName` → `company_name`)
   * Set **Assign To** to the user who should own the imported jobs
   * Toggle **Only Import New** to skip items older than today
6. The **For Each Item** and **Create Record** steps are pre-configured
7. Activate the automation

### Cron setup

Add an hourly cron job (Vercel Cron, GitHub Actions, or any scheduler) that calls:

```
POST https://your-app.com/api/cron/automations
Authorization: Bearer YOUR_CRON_SECRET
```

The endpoint checks each scheduled automation's interval and only runs it when it's due.

### Supported data formats

| Format | How items are extracted                                        |
| ------ | -------------------------------------------------------------- |
| JSON   | Resolves the `data_path` key and treats the result as an array |
| RSS    | Extracts `<item>` blocks from RSS XML                          |
| XML    | Extracts `<item>` or `<entry>` blocks from generic XML         |

***

## What Kardow Automations is good at

* Reacting to platform events with zero infrastructure overhead
* Chaining multiple steps including delays (backed by QStash — no cron jobs to manage)
* Connecting to any external API via webhook or HTTP request
* Extending with custom Cloudflare Workers for logic too specific to bake in
* Full run analytics: every execution is logged with status, timing, and error details

***

## Current limitations

<Warning>
  These are the current boundaries of the system. Some are planned improvements, some are by design.
</Warning>

| Area                      | Current state                                                                                                                                                                                                                        |
| ------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| **Branching**             | Only simple if/stop branching (Condition action). No true if/else fork with two separate paths.                                                                                                                                      |
| **Retry logic**           | Failed actions are logged but not automatically retried. You can re-trigger manually.                                                                                                                                                |
| **Scraper trigger**       | The scraper runs as a Supabase Edge Function. It calls the automation trigger endpoint via a lightweight `fetch` — it does not run the full engine inline.                                                                           |
| **Social media OAuth**    | Platform API credentials (Twitter, LinkedIn) must be managed by you. Kardow does not store OAuth tokens on your behalf yet.                                                                                                          |
| **Scheduled triggers**    | Fully supported. Use the `schedule.cron` trigger with interval config (hourly, every 6h, every 12h, daily, weekly). The `/api/cron/automations` endpoint runs all due scheduled automations — point your external cron at it hourly. |
| **Parallel actions**      | Actions execute in sequence, not in parallel.                                                                                                                                                                                        |
| **Action history replay** | You can see run logs but cannot replay or re-run a specific past execution from the UI yet.                                                                                                                                          |

***

## Planning for subscriptions, communities & monetization

### Resume database access

The payments and paywall system supports plan-gated content today. To gate a resume database:

1. Create a payment plan with a specific `code` (e.g., `resume_access`)
2. Use the Paywall feature to restrict the resume section to subscribers of that plan
3. Use an automation (`subscription.created`) to send them a welcome email or grant access to external tools

### Custom communities (Circle.so or built-in)

**Circle.so integration**: Works today via HTTP Request actions as described above. Full sync in both directions is possible with one automation per event (`subscription.created`, `subscription.cancelled`).

**Built-in community features**: Not in the current system. Would require new pages and a roles/access layer. The role system (employer, candidate, admin) is extensible — new roles can be added — but a full community module (posts, threads, spaces) would be a separate feature build.

### Multi-plan role permissions

The current subscription system supports plan types and job quotas. Expanding to role-based feature flags (e.g., "only users on the `community` plan can see the forum") can be done today by:

1. Checking `user_subscriptions.plan_type` in your pages
2. Using automations to sync plan type to a `user_metadata` field for fast client-side access

***

## Adding new trigger points

Any existing route can fire an automation in one line:

```ts theme={null}
import { triggerAutomation } from '@/lib/automations';

// After your existing logic:
triggerAutomation(supabase, {
  event: 'application.created',
  organization_id: orgId,
  payload: { job_id, applicant_email, job_title },
}).catch(console.error); // fire-and-forget, never blocks the response
```

No new API routes needed. No configuration changes. The engine finds all active automations for that event and org and runs them.

***

## Adding new action types

To add a new action type (e.g., `post_to_twitter`):

1. Add the key to `AutomationActionType` in `lib/automations/types.ts`
2. Add its label and description to `ACTION_TYPES`
3. Add a `case 'post_to_twitter':` block in the `executeAction` switch in `lib/automations/engine.ts`
4. Add a config form in `AutomationsClient.tsx`

That's it. The UI, API, and run logging all pick it up automatically.

***

## Security notes

* Automations are scoped to a single organization. You can never trigger another org's automations.
* All run logs (including payloads) are protected by RLS — only members of the org can read them.
* Webhook URLs and HTTP request targets are user-supplied — validate them in your config and consider allowlisting domains for sensitive actions.
* The trigger endpoint (`/api/automations/trigger`) uses admin Supabase access — it should not be called from client-side code.
