Skill: thierryteisseire/leadgenius-cli (version 1.0.2)

--- name: leadgenius-cli description: Operate the LeadGenius Pro Automation API and lgp CLI — ICP management, FSD pipeline automation, lead generation/enrichment/scoring, user provisioning, territory analysis, webhooks, and email platform integration. metadata: {"openclaw":{"emoji":"🎯","homepage":"https://github.com/thierryteisseire/leadgenius-cli","primaryEnv":"LGP_API_KEY","requires":{"env":["LGP_API_KEY"]}}} --- # LeadGenius Pro — CLI & Automation API Skill This skill teaches AI agents how to operate the **LeadGenius Pro Automation API** and the **`lgp` CLI tool**. It covers the full lifecycle of B2B lead management — from ICP (Ideal Customer Profile) definition and automated lead generation through enrichment, scoring, qualification, and email delivery via the FSD (Full-Stack Demand) pipeline. > **Documentation-only package.** The `lgp` CLI script (`lgp.py`) is part of the LeadGenius Pro application repository and is not included in this skill package. Obtain the CLI from your LeadGenius Pro deployment. ## Base URL ``` https://api.leadgenius.app ``` All API endpoints live under `/api/automation/`. ## Authentication Every request must include an API key in the `X-API-Key` header: ``` X-[REDACTED_SECRET] ``` - Keys are prefixed with `lgp_` and tied to a specific company. - The key determines `company_id`, `owner` identity, and rate limits. - Keys are created via `POST /api/automation/users/provision` — the plain-text key is returned only once at creation time. - Test your key with `GET /api/automation/auth/test`. ### Required Environment Variables | Variable | Required | Description | Default | |----------|----------|-------------|---------| | `LGP_API_KEY` | **Yes** | API key with `lgp_` prefix. Required for all API and CLI operations. | — | | `LGP_URL` | No | Base URL of the LeadGenius Pro API. | `http://localhost:3000` | | `LGP_ADMIN_KEY` | No | Admin key to bypass rate limits. Sent as `X-Admin-Key` header alongside `X-API-Key`. Grants elevated access — use only for admin operations. | — | ### Rate Limits | Window | Default Limit | |------------|------------------| | Per minute | 60 requests | | Per hour | 1,000 requests | | Per day | 10,000 requests | --- ## Prerequisites Checklist Before running enrichment, copyright, scoring, or FSD pipelines, the following configuration records must exist. Create them via the Tables API (`POST /api/automation/tables/{tableName}`). ### UrlSettings (required for enrichment) | Field | Description | |----------------------|------------------------------------------| | `companyUrl` | Company URL lookup service endpoint | | `companyUrl_Apikey` | API key for company URL service | | `emailFinder` | Email finder service endpoint | | `emailFinder_Apikey` | API key for email finder service | | `enrichment1`–`enrichment10` | Enrichment service endpoints (up to 10) | | `enrichment1_Apikey`–`enrichment10_Apikey` | Corresponding API keys | ### AgentSettings (required for copyright / AI content generation) | Field | Description | |--------------------------------|--------------------------------------------| | `projectId` | EpsimoAI project ID | | `enrichment1AgentId`–`enrichment10AgentId` | EpsimoAI agent IDs for each copyright process | ### SdrAiSettings (required for SDR AI scoring) | Field | Description | |--------------------------------|--------------------------------------------| | `projectId` | EpsimoAI project ID | | `aiLeadScoreAgentId` | Agent for lead scoring | | `aiQualificationAgentId` | Agent for qualification assessment | | `aiNextActionAgentId` | Agent for next-action recommendation | | `aiColdEmailAgentId` | Agent for cold email generation | | `aiInterestAgentId` | Agent for interest analysis | | `aiLinkedinConnectAgentId` | Agent for LinkedIn connect messages | | `aiCompetitorAnalysisAgentId` | Agent for competitor analysis | | `aiEngagementLevelAgentId` | Agent for engagement level assessment | | `aiPurchaseWindowAgentId` | Agent for purchase window estimation | | `aiDecisionMakerRoleAgentId` | Agent for decision-maker role detection | | `aiSentimentAgentId` | Agent for sentiment analysis | | `aiSocialEngagementAgentId` | Agent for social engagement scoring | | `aiNurturingStageAgentId` | Agent for nurturing stage classification | | `aiBudgetEstimationAgentId` | Agent for budget estimation | | `aiRiskScoreAgentId` | Agent for risk scoring | | `aiProductFitScoreAgentId` | Agent for product-fit scoring | ### ICP with Apify Config (required for lead generation) | Field | Description | |-------------------|--------------------------------------------------| | `name` | ICP display name | | `apifyActorId` | Apify actor ID for lead scraping (required) | | `apifyInput` | JSON string of actor input configuration | | `apifySettings` | JSON string of additional Apify settings | | `maxLeads` | Max leads per generation run (default 100) | | `industries` | JSON array of target industries | | `companySizes` | JSON array of size ranges ("1-10", "51-200") | | `geographies` | JSON array of countries/regions | | `jobTitles` | JSON array of target job titles | | `seniority` | JSON array of seniority levels | | `client_id` | Client partition for data isolation | ### Client (required for data isolation) | Field | Description | |---------------|--------------------------------------| | `clientName` | Display name for the client | | `companyURL` | Company website URL | | `description` | Client description | ### EmailPlatformSettings (required for email delivery) | Field | Description | |--------------|------------------------------------------| | `platform` | Email platform name (e.g., "woodpecker") | | `apiKey` | Platform API key | | `campaignId` | Default campaign ID on the platform | --- ## Quick-Reference: Endpoint Map | API Section | Reference | |---------------------|------------------------------------------------------------------| | Auth | [references/api_endpoints.md#authentication](references/api_endpoints.md#authentication) | | Leads | [references/api_endpoints.md#leads](references/api_endpoints.md#leads) | | Tasks | [references/api_endpoints.md#tasks](references/api_endpoints.md#tasks) | | Lead Generation | [references/api_endpoints.md#lead-generation](references/api_endpoints.md#lead-generation) | | Territory | [references/api_endpoints.md#territory-companies](references/api_endpoints.md#territory-companies) | | Webhooks | [references/api_endpoints.md#webhook-events](references/api_endpoints.md#webhook-events) | | Users | [references/api_endpoints.md#users](references/api_endpoints.md#users) | | Organizations | [references/api_endpoints.md#organizations](references/api_endpoints.md#organizations) | | Tables / ICP | [references/api_endpoints.md#tables-generic-crud-with-icp-focus](references/api_endpoints.md#tables-generic-crud-with-icp-focus) | | Email Platforms | [references/api_endpoints.md#email-platforms](references/api_endpoints.md#email-platforms) | | FSD Pipeline | [references/api_endpoints.md#fsd-pipeline](references/api_endpoints.md#fsd-pipeline) | | Job Ad Lead Triggering | [references/api_endpoints.md#job-ad-lead-triggering](references/api_endpoints.md#job-ad-lead-triggering) | | Lead Notifications (Unipile) | [references/api_endpoints.md#lead-notifications-unipile](references/api_endpoints.md#lead-notifications-unipile) | | EpsimoAI | [references/api_endpoints.md#epsimoai-user--credit-management](references/api_endpoints.md#epsimoai-user--credit-management) | | Account-Based Lead Analysis | [references/api_endpoints.md#account-based-lead-analysis](references/api_endpoints.md#account-based-lead-analysis) | | Error Codes | [references/api_endpoints.md#error-codes](references/api_endpoints.md#error-codes) | ## Quick-Reference: CLI Map | Command Group | Reference | |--------------------|--------------------------------------------------------------------| | `auth` | [references/cli_reference.md#auth](references/cli_reference.md#auth) | | `leads` | [references/cli_reference.md#leads](references/cli_reference.md#leads) | | `tasks` | [references/cli_reference.md#tasks](references/cli_reference.md#tasks) | | `generate` | [references/cli_reference.md#generate](references/cli_reference.md#generate) | | `clients` | [references/cli_reference.md#clients](references/cli_reference.md#clients) | | `maintenance` | [references/cli_reference.md#maintenance](references/cli_reference.md#maintenance) | | `pipeline` | [references/cli_reference.md#pipeline](references/cli_reference.md#pipeline) | | `campaigns` | [references/cli_reference.md#campaigns](references/cli_reference.md#campaigns) | | `companies` | [references/cli_reference.md#companies](references/cli_reference.md#companies) | | `webhooks` | [references/cli_reference.md#webhooks](references/cli_reference.md#webhooks) | | `tables` | [references/cli_reference.md#tables](references/cli_reference.md#tables) | | `email-platforms` | [references/cli_reference.md#email-platforms](references/cli_reference.md#email-platforms) | | `users` | [references/cli_reference.md#users](references/cli_reference.md#users) | | `cognito` | [references/cli_reference.md#cognito](references/cli_reference.md#cognito) | | `org` | [references/cli_reference.md#org](references/cli_reference.md#org) | | `fsd` | [references/cli_reference.md#fsd](references/cli_reference.md#fsd) | | `epsimo` | [references/cli_reference.md#epsimoai-commands](references/cli_reference.md#epsimoai-commands) | | `admin` | [references/cli_reference.md#admin](references/cli_reference.md#admin) | | `account-analysis` | [references/cli_reference.md#account-analysis](references/cli_reference.md#account-analysis) | --- ## Maintenance CLI (Standalone Node.js Scripts) Standalone scripts for managing maintenance items (bugs, enhancements). These use raw GraphQL over HTTPS with API key auth from `amplify_outputs.json` — they do NOT go through the Automation API or `lgp` CLI. ### Create a maintenance item ```bash node scripts/create-maintenance-item.js --type=BUG --description="Login fails on mobile" node scripts/create-maintenance-item.js --type=ENHANCEMENT --description="Add bulk export feature" ``` | Flag | Required | Values | Description | |------|----------|--------|-------------| | `--type` | Yes | `BUG`, `ENHANCEMENT` | Item type | | `--description` | Yes | string | Description of the issue or request | ### List maintenance items ```bash node scripts/list-maintenance-items.js node scripts/list-maintenance-items.js --type=BUG node scripts/list-maintenance-items.js --type=BUG --status=OPEN node scripts/list-maintenance-items.js --company-id=company-xxx ``` | Flag | Required | Values | Description | |------|----------|--------|-------------| | `--type` | No | `BUG`, `ENHANCEMENT` | Filter by type | | `--status` | No | `OPEN`, `IN_PROGRESS`, `RESOLVED`, `CLOSED` | Filter by status | | `--company-id` | No | string | Filter by company ID | ### Update a maintenance item ```bash node scripts/update-maintenance-item.js --id=<UUID> --status=RESOLVED node scripts/update-maintenance-item.js --id=<UUID> --status=CLOSED --description="Fixed in v2.1" ``` | Flag | Required | Values | Description | |------|----------|--------|-------------| | `--id` | Yes | UUID | Full maintenance item ID | | `--status` | No | `OPEN`, `IN_PROGRESS`, `RESOLVED`, `CLOSED` | New status | | `--description` | No | string | Updated description | ### Maintenance REST API There is also a REST API at `/api/maintenance` supporting full CRUD with JWT or API key auth. See the `leadgenius-api` skill for details. --- ## Documentation Site Docsify-based docs are served at `/docs` (e.g., `https://api.leadgenius.app/docs`). Source files in `/docs/`, copied to `public/docs-content/` at build time. --- ## API Behavioral Notes (2026-03-27) Important behavioral changes that affect CLI and API consumers: ### Leads List Pagination (GET /api/automation/leads) - **Max page size capped at 50.** The `limit` parameter is clamped to 50 regardless of the value passed. Callers must paginate via `nextToken` to retrieve all leads. - **Orphaned leads mode:** Pass `client_id=__orphaned__` to find leads with null/empty `client_id` within your company. These leads are invisible to normal GSI queries. - **Cross-tenant validation:** The API now validates that the requested `client_id` belongs to your company before querying. Foreign `client_id` values return 403. ### Lead Import Upsert (POST /api/automation/leads/import) - **Idempotent by email:** If a lead with the same `email` already exists in the **same** `client_id`, the import updates the existing record instead of creating a duplicate. This makes retries after ECONNRESET safe. - **Explicit ID upsert:** If the payload includes an `id` field that matches an existing lead, it updates instead of crashing with a DynamoDB conditional error. - **Empty strings → null:** Empty string values (`""`) are automatically converted to `null` before write to prevent DynamoDB GSI `ValidationException` errors. - **Response always includes `errors` and `warnings` arrays** (even when empty) for consistent CLI parsing. ### Lead Transfer (POST /api/automation/leads/transfer) - **`skipSourceValidation: true`** — Allows transferring from an orphaned/deleted source client that no longer has a Client record. - **`fromClientId: "__orphaned__"`** — Transfers leads with null/empty `client_id` to a valid target client. ### Generic Tables (POST /api/automation/tables/{tableName}) - **Client table auto-generates `client_id`** if not provided in the body, matching the behavior of `POST /api/clients`. - **Tables without `client_id`** (Maintenance, Company, CompanyUser, etc.) no longer crash — the field is automatically stripped before the GraphQL mutation. - **Table-specific fields** are now included in list/create responses (e.g., `clientName` for Client, `name` for ICP, `type`/`description`/`status` for Maintenance). ### Cross-Tenant Safety - **Single lead GET** (`GET /api/automation/leads/{id}`) flags cross-tenant `client_id` references with a `_clientWarning` field and prefixes the value with `__foreign:`. - All lead PUT/DELETE operations cascade across EnrichLeads, B2BLeads, and SourceLeads tables transparently.

path: README.md
content: # LeadGenius Pro — CLI & Automation API

A comprehensive skill for operating the LeadGenius Pro Automation API and the `lgp` CLI tool. Covers the full lifecycle of B2B lead management — from ICP definition and automated lead generation through enrichment, scoring, qualification, and email delivery via the FSD (Full-Stack Demand) pipeline.

## Table of Contents

- [Overview](#overview)
- [Installation & Setup](#installation--setup)
- [Authentication](#authentication)
- [API Endpoint Groups](#api-endpoint-groups)
  - [Auth](#auth)
  - [Leads](#leads)
  - [Tasks (Background Processing)](#tasks-background-processing)
  - [Territory Companies](#territory-companies)
  - [Webhook Events](#webhook-events)
  - [Users](#users)
  - [Organizations](#organizations)
  - [Cognito (User Pool)](#cognito-user-pool)
  - [Tables (Generic CRUD)](#tables-generic-crud)
  - [Email Platforms](#email-platforms)
  - [FSD Pipeline](#fsd-pipeline)
  - [EpsimoAI](#epsimoai)
- [CLI Command Reference](#cli-command-reference)
- [Business Workflows](#business-workflows)
- [Business Use Cases](#business-use-cases)
- [Error Handling](#error-handling)
- [Prerequisites Checklist](#prerequisites-checklist)
- [Reference Files](#reference-files)

---

## Overview

LeadGenius Pro is a B2B lead management and enrichment platform. The Automation API exposes the full platform functionality via REST endpoints, and the `lgp` CLI wraps those endpoints for terminal-based workflows.

The platform pipeline:

```
Source Leads → Import → Enrich → Score → Qualify → Deliver to Email Platform
```

The FSD (Full-Stack Demand) pipeline automates this end-to-end:

```
ICP Definition → Apify Lead Generation → Enrichment → SDR AI Scoring → Qualification → Email Delivery
```

**Base URL:** `https://api.leadgenius.app`

All API endpoints live under `/api/automation/`.

---

## Installation & Setup

> **Note:** This skill package is documentation-only. The `lgp` CLI script (`lgp.py`) is part of the LeadGenius Pro application repository and is **not included** in this package. Obtain the CLI from your LeadGenius Pro deployment (typically at `.agent/skills/leadgenius-api/scripts/lgp.py` in the application workspace).

The CLI is a Python script requiring Python 3.8+ and the `requests` library (`pip install requests`):

```bash
python lgp.py <command> [options]
```

### Required Environment Variables

| Variable | Required | Description | Default |
|----------|----------|-------------|---------|
| `LGP_API_KEY` | **Yes** | API key with `lgp_` prefix. Created via `POST /api/automation/users/provision`. | — |
| `LGP_URL` | No | Base URL of the LeadGenius API | `http://localhost:3000` |
| `LGP_ADMIN_KEY` | No | Admin key to bypass rate limits. Sent as `X-Admin-Key` header alongside `X-API-Key`. Grants elevated access — use only for admin operations. | — |

```bash
export LGP_API_KEY="lgp_your_key_here"
export LGP_URL="https://api.leadgenius.app"
# Optional — only if you need admin-level rate limit bypass:
# export LGP_ADMIN_KEY="your_admin_key_here"
```

### Global CLI Options

| Option | Default | Description |
|--------|---------|-------------|
| `--api-key <key>` | `LGP_API_KEY` env | Override API key |
| `--admin-key <key>` | `LGP_ADMIN_KEY` env | Override admin key (bypasses rate limits) |
| `--url <url>` | `LGP_URL` env | Override base URL |
| `--format <fmt>` | `json` | Output: `json` or `table` |

---

## Authentication

Every request requires an `X-API-Key` header:

```
X-[REDACTED_SECRET]
```

- Keys are prefixed with `lgp_` and tied to a specific company
- The key determines `company_id`, `owner` identity, and rate limits
- Keys are created via `POST /api/automation/users/provision` — the plain-text key is returned only once
- Test your key: `GET /api/automation/auth/test`

### Rate Limits

| Window | Limit |
|--------|-------|
| Per minute | 60 requests |
| Per hour | 1,000 requests |
| Per day | 10,000 requests |

---

## API Endpoint Groups

### Auth

| Method | Endpoint | Description |
|--------|----------|-------------|
| `GET` | `/api/automation/auth/test` | Verify API key validity, returns owner/company/keyId |

```bash
lgp auth test
```

---

### Leads

Full lead lifecycle: list, get, import, search, deduplicate, transfer, and engagement tracking.

| Method | Endpoint | Description |
|--------|----------|-------------|
| `GET` | `/api/automation/leads` | List leads by `client_id` with field selection and pagination |
| `GET` | `/api/automation/leads/{id}` | Get single lead (100+ fields) |
| `POST` | `/api/automation/leads/import` | Import leads (single or batch up to 500) |
| `GET` | `/api/automation/leads/search` | Search by email, name, companyUrl, linkedinUrl |
| `POST` | `/api/automation/leads/deduplicate` | Find duplicate groups by match fields |
| `POST` | `/api/automation/leads/deduplicate/resolve` | Merge duplicates into a keep lead |
| `POST` | `/api/automation/leads/transfer` | Transfer leads between clients with dedup detection |
| `GET` | `/api/automation/leads/{id}/activities` | Get engagement history |
| `POST` | `/api/automation/leads/{id}/activities` | Log engagement activities (single or batch) |
| `POST` | `/api/automation/leads/validate-ownership` | Scan for orphaned/mismatched leads |

**Key behaviors:**
- `owner` and `company_id` are auto-set from API key on import
- Cross-client duplicate detection by email/linkedinUrl (warning only)
- Batch import max: 500 leads
- Field selection: pass `fields=email,companyName,aiLeadScore` to return only those fields
- Pagination via `nextToken`

**Deduplication match fields:**

| Field | Confidence |
|-------|------------|
| `email` | high |
| `linkedinUrl` | medium |
| `fullName+companyName` | low |

**Engagement activity types:** `linkedin_connection_sent`, `linkedin_connection_accepted`, `linkedin_message_sent`, `linkedin_message_received`, `linkedin_profile_viewed`, `email_sent`, `email_opened`, `email_clicked`, `email_answered`, `email_bounced`, `call_completed`, `call_no_answer`, `meeting_scheduled`, `meeting_completed`, `form_submitted`, `website_visited`, `content_downloaded`, `demo_requested`, `proposal_sent`, `contract_signed`, `custom`

**Engagement score weights (time-decayed, capped at 100):**

| Activity | Points |
|----------|--------|
| `contract_signed`, `meeting_completed` | 30 |
| `meeting_scheduled`, `demo_requested` | 25 |
| `email_answered`, `proposal_sent`, `form_submitted` | 20 |
| `linkedin_message_received`, `call_completed` | 15 |
| `linkedin_connection_accepted`, `content_downloaded` | 10 |
| `email_clicked` | 8 |
| `email_opened`, `custom` | 5 |
| `linkedin_message_sent`, `email_sent`, `website_visited` | 2–3 |
| `linkedin_connection_sent`, `linkedin_profile_viewed`, `call_no_answer` | 1–2 |
| `email_bounced` | -5 |

Activities < 30 days old get full weight; older activities get 50%.


---

### Tasks (Background Processing)

Enrichment, copyright (AI content), and scoring jobs run asynchronously via Trigger.dev.

| Method | Endpoint | Description |
|--------|----------|-------------|
| `GET` | `/api/automation/tasks` | List jobs (filter by status/type) |
| `GET` | `/api/automation/tasks/{jobId}` | Get job status and progress |
| `POST` | `/api/automation/tasks/enrich` | Trigger enrichment for a lead |
| `POST` | `/api/automation/tasks/copyright` | Trigger AI content generation for a lead |
| `POST` | `/api/automation/tasks/score` | Trigger SDR AI scoring (single or batch up to 100) |

**Enrichment services:** `companyUrl`, `emailFinder`, `enrichment1`–`enrichment10` (configured in UrlSettings)

**Copyright processes:** `enrichment1`–`enrichment10` (each maps to an AI agent in AgentSettings)

**Scoring fields:** `aiLeadScore`, `aiQualification`, `aiNextAction`, `aiColdEmail`, `aiInterest`, `aiLinkedinConnect`, `aiCompetitorAnalysis`, `aiEngagementLevel`, `aiPurchaseWindow`, `aiDecisionMakerRole`, `aiSentiment`, `aiSocialEngagement`, `aiNurturingStage`, `aiBudgetEstimation`, `aiRiskScore`, `aiProductFitScore`

---

### Territory Companies

Company-level intelligence aggregated from lead data.

| Method | Endpoint | Description |
|--------|----------|-------------|
| `GET` | `/api/automation/companies` | List territory companies (sort, filter by industry/country/score) |
| `GET` | `/api/automation/companies/{id}` | Get company detail with content analysis and leads summary |
| `GET` | `/api/automation/companies/{id}/leads` | List leads for a company |
| `POST` | `/api/automation/companies/aggregate` | Aggregate leads into TerritoryCompany records |
| `POST` | `/api/automation/companies/{id}/content-analysis` | Re-run content analysis |
| `GET` | `/api/automation/companies/events` | List company events |
| `POST` | `/api/automation/companies/events` | Create manual event |
| `DELETE` | `/api/automation/companies/events` | Batch delete events |
| `POST` | `/api/automation/companies/events/generate` | Auto-generate events from lead activity |
| `GET` | `/api/automation/companies/events/radar` | Radar dashboard summary |

**Content analysis fields:** `contentTopics`, `contentKeywords`, `leadTitles`, `leadHeadlines`, `targetAudiences`, `painPoints`, `valuePropositions`, `competitorMentions`, `engagementInsights`, `contentRecommendations`

**Event types:** `new_lead`, `lead_qualified`, `score_increased`, `new_company`, `custom`

---

### Webhook Events

Inbound webhook event management from email platforms (Woodpecker, Lemlist, etc.).

| Method | Endpoint | Description |
|--------|----------|-------------|
| `GET` | `/api/automation/webhook-events` | List events (filter by platform/type/status/date) |
| `GET` | `/api/automation/webhook-events/{id}` | Get event detail with raw payload |
| `PUT` | `/api/automation/webhook-events/{id}` | Update event fields |
| `DELETE` | `/api/automation/webhook-events` | Batch delete events |
| `POST` | `/api/automation/webhook-events/{id}/reprocess` | Re-run lead matching |

**Lead matching priority on reprocess:**

| Priority | Method | Confidence | GSI |
|----------|--------|------------|-----|
| 1 | Email | high | `email-index` |
| 2 | LinkedIn URL | medium | `company_id` GSI + filter |
| 3 | First + Last name | low | `firstName-lastName-index` |

---

### Users

Company user management with role-based access control.

| Method | Endpoint | Description |
|--------|----------|-------------|
| `GET` | `/api/automation/users` | List company users (filter by group) |
| `GET` | `/api/automation/users/{id}` | Get user detail |
| `POST` | `/api/automation/users` | Create/invite user |
| `PUT` | `/api/automation/users/{id}` | Update role/group/status/permissions |
| `DELETE` | `/api/automation/users/{id}` | Remove user |
| `GET` | `/api/automation/users/menu-config` | Get menu keys and group defaults |
| `POST` | `/api/automation/users/provision` | One-shot: Cognito + Company + CompanyUser + API key |

**Roles:** `owner`, `admin`, `member`, `viewer`
**Groups:** `admin`, `manager`, `user`, `viewer`
**Client access modes:** `all`, `own`, `specific`

**Provisioning** creates everything in one call — Cognito user, company (new or join existing), CompanyUser record, and API key. The `plainTextKey` is returned only once.

---

### Organizations

Company (organization) management.

| Method | Endpoint | Description |
|--------|----------|-------------|
| `GET` | `/api/automation/companies/manage` | List companies |
| `GET` | `/api/automation/companies/manage?id={id}` | Get company |
| `POST` | `/api/automation/companies/manage` | Create company |
| `PUT` | `/api/automation/companies/manage` | Rename / add-user / update-user / remove-user |
| `DELETE` | `/api/automation/companies/manage?id={id}` | Delete company |
| `GET` | `/api/automation/companies/manage?id={id}&users=true` | List company users |

---

### Cognito (User Pool)

Low-level AWS Cognito user pool operations.

| Method | Endpoint | Description |
|--------|----------|-------------|
| `GET` | `/api/automation/users/cognito` | Look up user by email or list users |
| `POST` | `/api/automation/users/cognito` | Create Cognito user with permanent password |
| `PUT` | `/api/automation/users/cognito` | Enable/disable/set-password/set-attributes |

---

### Tables (Generic CRUD)

Generic CRUD for any supported DynamoDB table. Primary interface for configuration records and business data models.

| Method | Endpoint | Description |
|--------|----------|-------------|
| `GET` | `/api/automation/tables/{tableName}` | List records (paginated) |
| `POST` | `/api/automation/tables/{tableName}` | Create record |
| `GET` | `/api/automation/tables/{tableName}/{id}` | Get record by ID |
| `PUT` | `/api/automation/tables/{tableName}/{id}` | Update record |
| `DELETE` | `/api/automation/tables/{tableName}/{id}` | Delete record |

**Supported tables:**

| Category | Tables |
|----------|--------|
| Multi-Tenant | `Company`, `CompanyUser`, `CompanyInvitation` |
| Core Data | `Jobs`, `B2BLeads`, `EnrichLeads`, `SourceLeads`, `TerritoryCompany`, `CompanyEvent`, `LinkedInJobs` |
| Campaign & Targeting | `ICP`, `ABMCampaign`, `TargetAccount` |
| Outreach & Workflow | `OutreachSequence`, `SequenceEnrollment`, `Workflow`, `WorkflowExecution` |
| Webhook & Integration | `Integration`, `Webhook`, `WebhookLog`, `WebhookSettings`, `InboundWebhook`, `WebhookEvent` |
| AI & Agent Config | `Agent`, `AgentSettings`, `SdrAiSettings`, `CopyrightSettings`, `SdrSettings` |
| Service Config | `EnrichmentService`, `EmailPlatformSettings`, `OutreachCampaign`, `OutreachTemplate`, `BaserowSyncConfig`, `BaserowSyncHistory`, `BaserowConfig`, `UnipileSettings`, `UnipileAccount`, `UnipileMessage`, `UnipileChat`, `UnipileLog`, `UnipileCampaign`, `UnipileIntegration` |
| System | `AgentApiKey`, `UrlSettings`, `Client`, `SearchHistory`, `Maintenance`, `SidebarConfig`, `SharedView` |

**ICP field groups:**
- Targeting: `name`, `description`, `industries`, `companySizes`, `geographies`, `jobTitles`, `seniority`, `departments`, `functions`, `keywords`, `technologies`
- Apify config: `apifyActorId` (required for generation), `apifyInput`, `apifySettings`, `maxLeads`, `leadSources`
- Qualification: `qualificationCriteria`, `scoringWeights`
- Metadata: `isActive`, `lastUsedDate`, `totalLeadsGenerated`, `client_id`


---

### Email Platforms

Email platform integration for outbound lead delivery.

| Method | Endpoint | Description |
|--------|----------|-------------|
| `GET` | `/api/automation/email-platforms` | List configured platforms with connection status |
| `POST` | [REDACTED_SECRET] | Send leads to a platform campaign (max 200) |

**Send behavior:**
- Platform must be active (`isActive: true`)
- Prefers `emailFinder` field over `email` for verified addresses
- Leads without email are skipped with `missing_email` reason

---

### FSD Pipeline

Full-Stack Demand generation — create campaigns, run lead generation pipelines, and monitor stage-by-stage progress.

| Method | Endpoint | Description |
|--------|----------|-------------|
| `GET` | `/api/automation/fsd/campaigns` | List campaigns with metrics |
| `GET` | `/api/automation/fsd/campaigns/{id}` | Get campaign detail |
| `POST` | `/api/automation/fsd/campaigns` | Create campaign |
| `PUT` | `/api/automation/fsd/campaigns/{id}` | Update campaign settings |
| `DELETE` | `/api/automation/fsd/campaigns/{id}` | Soft-delete (deactivate) campaign |
| `POST` | `/api/automation/fsd/run` | Start pipeline run |
| `GET` | `/api/automation/fsd/run/{pipelineId}` | Get pipeline status and metrics |

**Pipeline stage progression:**

```
generating → enriching → scoring → qualifying → sending → completed
                                                            ↘ failed
```

| Stage | Description | Metric |
|-------|-------------|--------|
| `generating` | Apify actor scraping leads | `leadsGenerated` |
| `enriching` | Enrichment services running | `leadsEnriched` |
| `scoring` | SDR AI scoring leads | `leadsScored` |
| `qualifying` | Filtering by threshold | `leadsQualified` |
| `sending` | Delivering to email platform | `leadsSent` |
| `completed` | Pipeline finished | — |
| `failed` | Error occurred | `errorMessage`, `stageErrors` |

**Automation flags:**
- `enrichAfterGeneration: true` → auto-enrich after generation
- `scoreAfterEnrichment: true` → auto-score after enrichment
- `sendToEmailPlatform` + `qualificationThreshold` → auto-filter and deliver

**ICP-to-Generation flow:** When `icpId` is provided, the API resolves `apifyActorId` and `apifyInput` from the ICP record automatically. Alternative: provide `apifyActorId` and `apifyInput` directly.

**Campaign fields:** `name`, `client_id`, `icpId`, `apifyActorId`, `apifyInput`, `frequency` (`once`/`daily`/`weekly`/`monthly`), `targetLeadCount`, `enrichAfterGeneration`, `scoreAfterEnrichment`, `sendToEmailPlatform`, `qualificationThreshold`, `emailCampaignId`, `isActive`

---

### EpsimoAI

EpsimoAI user activation, profile, credit management, and thread usage.

| Method | Endpoint | Description |
|--------|----------|-------------|
| `POST` | `/api/automation/epsimo/users/activate` | Login or Cognito token exchange |
| `GET` | `/api/automation/epsimo/users/info` | User profile with derived plan |
| `GET` | `/api/automation/epsimo/credits/balance` | Credit balance (remaining threads) |
| `POST` | `/api/automation/epsimo/credits/purchase` | Purchase credits |
| `GET` | `/api/automation/epsimo/threads` | Thread usage with percentage and plan |

**Token convention:** `X-Epsimo-Token` header (preferred) or `epsimoToken` query parameter. The activate endpoint is the only one that doesn't require a token.

**Auth order:** All epsimo routes validate: (1) `X-API-Key` → `AUTH_MISSING`/`AUTH_INVALID`, (2) EpsimoAI token → `MISSING_EPSIMO_TOKEN`, (3) upstream call.

**Plan derivation logic (`derivePlan`):**

| Condition | Plan |
|-----------|------|
| threadMax >= 120,000 | enterprise |
| threadMax >= 50,000 OR stripeClientId is truthy | premium |
| threadMax >= 10,000 | pro |
| default | free |

Note: `/users/info` uses `derivePlan(threadMax, stripeClientId)` (full), while `/threads` uses `derivePlan(threadMax)` only — plan may differ between endpoints for users with `stripeClientId` and low `threadMax`.

**Credit balance:** `credits = Math.max(0, thread_max - thread_counter)` — never negative.

**Usage percentage:** `Math.round((thread_counter / thread_max) * 100 * 100) / 100` (0 if threadMax is 0).

**Purchase validation:** `amount` must be present, `Number.isInteger(amount)`, and `> 0`. Strings, floats, zero, and negatives all fail.

**UI route:** `POST /api/credits/purchase` — cookie-based (`epsimo_token` cookie), flat response (no standard envelope), used by frontend `CreditService.purchaseCredits()`.

---

## CLI Command Reference

All commands use the syntax: `python lgp.py <group> <command> [options]`

### auth

| Command | Description |
|---------|-------------|
| `auth test` | Verify API key validity |

### leads

| Command | Description |
|---------|-------------|
| `leads list --client <id>` | List leads (supports `--limit`, `--fields`, `--next-token`) |
| `leads get <id>` | Get lead detail |
| `leads import --file <path>` or `--data '<json>'` | Import leads |
| `leads search` | Search by `--email`, `--first-name`, `--last-name`, `--company-url`, `--linkedin-url` |
| `leads dedup --client <id> --match <fields>` | Find duplicates |
| `leads dedup-resolve --keep <id> --merge <ids>` | Merge duplicates |
| `leads transfer --from <id> --to <id>` | Transfer leads (`--all`, `--dry-run`) |
| `leads validate-ownership` | Scan for ownership issues |
| `leads activity <leadId> --type <type>` | Log engagement (`--notes`, `--metadata`) |
| `leads activities <leadId>` | Get engagement history |

### tasks

| Command | Description |
|---------|-------------|
| `tasks list` | List jobs (`--status`, `--type`, `--limit`) |
| `tasks status <jobId>` | Get job status |
| `tasks enrich --lead <id>` | Trigger enrichment (`--services`) |
| `tasks copyright --lead <id>` | Trigger AI content (`--processes`) |
| `tasks score --lead <id>` | Trigger scoring (`--fields`) |

### companies

| Command | Description |
|---------|-------------|
| `companies list --client <id>` | List territory companies (`--sort`, `--limit`) |
| `companies get <id>` | Get company detail |
| `companies leads <id>` | List company leads |
| `companies content-analysis <id>` | Re-run content analysis |

### webhooks

| Command | Description |
|---------|-------------|
| `webhooks list` | List events (`--platform`, `--event-type`, `--limit`) |
| `webhooks get <id>` | Get event detail |
| `webhooks reprocess <id>` | Re-run lead matching |

### tables

| Command | Description |
|---------|-------------|
| `tables list <tableName>` | List records (`--limit`) |
| `tables create <tableName> --data '<json>'` | Create record |
| `tables get <tableName> <id>` | Get record |
| `tables update <tableName> <id> --data '<json>'` | Update record |
| `tables delete <tableName> <id>` | Delete record |

### email-platforms

| Command | Description |
|---------|-------------|
| `email-platforms list` | List configured platforms |
| `email-platforms send` | Send leads (`--platform`, `--campaign`, `--leads`) |

### users

| Command | Description |
|---------|-------------|
| `users list` | List users (`--group`, `--limit`) |
| `users get <id>` | Get user detail |
| `users create --email <email>` | Create user (`--role`, `--group`) |
| `users update <id>` | Update user (`--role`, `--group`, `--status`) |
| `users delete <id>` | Remove user |
| `users provision --email <email> --password <pwd>` | Full provisioning (`--company-name` or `--company-id`) |

### cognito

| Command | Description |
|---------|-------------|
| `cognito list` | List Cognito users |
| `cognito get --email <email>` | Get user by email |
| `cognito create --email <email> --password <pwd>` | Create user |
| `cognito enable --email <email>` | Enable user |
| `cognito disable --email <email>` | Disable user |

### org

| Command | Description |
|---------|-------------|
| `org list` | List companies |
| `org get <id>` | Get company |
| `org create --name <name>` | Create company |
| `org rename <id> --name <name>` | Rename company |
| `org delete <id>` | Delete company |
| `org users <companyId>` | List company users |
| `org add-user <companyId> --email <email>` | Add user (`--role`, `--group`) |
| `org update-user <userId>` | Update user (`--role`, `--group`, `--status`) |
| `org remove-user <userId>` | Remove user |

### fsd

| Command | Description |
|---------|-------------|
| `fsd campaigns` | List campaigns |
| `fsd campaign <id>` | Get campaign detail |
| `fsd create-campaign --client <id> --name <name>` | Create campaign (`--icp`, `--frequency`, `--target`) |
| `fsd update-campaign <id>` | Update campaign (`--name`, `--target`, `--frequency`) |
| `fsd deactivate-campaign <id>` | Soft-delete campaign |
| `fsd run --client <id>` | Start pipeline (`--icp` or `--actor`/`--input`, `--target`) |
| `fsd status <pipelineId>` | Get pipeline status |

### epsimo

| Command | Description |
|---------|-------------|
| `epsimo activate --email <email> --password <pwd>` | Login mode |
| `epsimo activate --cognito-token <token>` | Exchange mode |
| `epsimo info --token <token>` | User profile and plan |
| `epsimo credits --token <token>` | Credit balance |
| `epsimo purchase --token <token> --amount <n>` | Purchase credits |
| `epsimo threads --token <token>` | Thread usage and percentage |


---

## Business Workflows

Detailed step-by-step operational workflows for multi-step processes. Full details in [references/workflows.md](references/workflows.md).

### 1. Lead Deduplication

Find and merge duplicate leads within a client. Steps: dry-run scan → review groups → resolve (merge) → verify.

**Match fields:** `email` (high), `linkedinUrl` (medium), `fullName+companyName` (low).

**Merge rules:** Only empty fields on the keep lead are filled. System fields never merged. First merge lead's value wins for ties. Keep lead's value always preserved on conflicts.

### 2. Lead Transfer

Transfer leads between clients with duplicate detection. Steps: dry-run → review skipped duplicates → execute → verify.

**Rules:** Both clients must belong to your company. Duplicates in target are skipped (not overwritten). Only `client_id` is updated.

### 3. Webhook Reprocessing

Re-match unmatched webhook events to leads after new imports. Steps: list unmatched → reprocess each → verify matches → clean up.

**Matching:** Uses `normalizedData` from the webhook. Tries email → LinkedIn URL → name in priority order. Updates `engagementHistory` and recalculates `engagementScore` on match.

### 4. Territory Analysis

Build company-level intelligence from lead data. Steps: aggregate → list companies → view detail → content analysis → generate events → radar dashboard.

**Aggregation:** Creates TerritoryCompany records from unique `companyName` values within a client, with metrics like `totalLeads`, `qualifiedLeads`, `averageLeadScore`.

### 5. Engagement Tracking

Log activities on leads and understand score calculation. Steps: log activity → batch log → retrieve history → verify score.

**Score:** Weighted by activity type with time decay (< 30 days = full, > 30 days = 50%). Capped at 100.

### 6. Data Health Check

Validate lead ownership and data integrity. Steps: run validation → review issues → fix orphaned records → verify client-company relationships → re-validate.

**Checks:** Orphaned leads (invalid `client_id`), mismatched company, null owner.

### 7. ICP-First Campaign Setup

Define ICP → validate Apify config → create FSD campaign → run pipeline → monitor → verify leads.

**ICP validation:** Must have `apifyActorId` set. Pipeline resolves Apify config from ICP automatically.

### 8. Multi-ICP Campaign Strategy

Create multiple ICPs for different segments → separate campaigns per ICP → run pipelines → compare results → deactivate underperformers.

### 9. Iterative ICP Refinement

Run initial generation → review scores → record baseline → update ICP criteria → run refined generation → compare before/after.

### 10. Full Automation Setup

Configure all prerequisites (UrlSettings, AgentSettings, SdrAiSettings, EmailPlatformSettings) → create ICP → create campaign with all automation flags → run and verify end-to-end.

### 11. Campaign Monitoring Dashboard

List active campaigns → check pipeline status → review per-stage metrics → diagnose failures → rerun failed pipelines → deactivate problematic campaigns.

### 12. Qualification and Delivery

Review scored leads → understand threshold → verify email platform config → check delivery metrics → verify via webhook events → manual send fallback.

---

## Business Use Cases

End-to-end business scenario guides. Full details in [references/use_cases.md](references/use_cases.md).

### 1. New Customer Onboarding

Complete environment setup: provision user → create client → configure UrlSettings → configure AgentSettings → configure SdrAiSettings → create ICP → run first FSD pipeline → monitor.

**Outcome:** Cognito user, company, client, all config records, active ICP, and a completed pipeline run with generated/enriched/scored leads.

### 2. ICP-Driven Campaign Launch

Define ICP with targeting + Apify config → create FSD campaign → run pipeline → monitor stages → review leads → verify email delivery.

**Outcome:** ICP record, FSD campaign, completed pipeline, leads delivered to email platform, webhook events tracking engagement.

### 3. Lead Qualification Pipeline

Import raw leads → enrich → copyright (AI content) → score → filter by threshold → send to email platform.

**Outcome:** Each lead progresses through import → enrichment → AI content → scoring → qualification → delivery. Full pipeline from raw data to outreach-ready leads.

---

## Error Handling

All errors return a consistent JSON envelope:

```json
{
  "success": false,
  "error": "Human-readable message",
  "details": "Additional context",
  "code": "MACHINE_READABLE_CODE",
  "requestId": "uuid-v4"
}
```

| Code | HTTP | Description |
|------|------|-------------|
| `UNAUTHORIZED` | 401 | Missing or invalid API key |
| `RATE_LIMITED` | 429 | Rate limit exceeded (60/min, 1000/hr, 10000/day) |
| `MISSING_CLIENT_ID` | 400 | Required `client_id` not provided |
| `CLIENT_WRONG_COMPANY` | 400/403 | Client belongs to a different company |
| `NOT_FOUND` | 404 | Resource doesn't exist or belongs to another company |
| `VALIDATION_ERROR` | 400 | Invalid field types, missing required values |
| `INVALID_BODY` | 400 | Request body is not valid JSON |
| `IMMUTABLE_FIELD` | 400 | Attempted to modify `owner` or `company_id` |
| `ICP_NOT_FOUND` | 404 | ICP record not found |
| `ICP_NO_APIFY` | 400 | ICP missing `apifyActorId` |
| `ICP_LOOKUP_FAILED` | 500 | Internal error resolving ICP |

**EpsimoAI-specific errors:**

| Code | HTTP | Description |
|------|------|-------------|
| `EPSIMO_AUTH_FAILED` | 401 | Login credentials invalid |
| `EPSIMO_TOKEN_INVALID` | 401 | Token expired or invalid (info endpoint only) |
| `EPSIMO_UNAVAILABLE` | 502 | Backend unreachable or 5xx |
| `EPSIMO_PURCHASE_FAILED` | 402 | Credit purchase rejected |
| `MISSING_EPSIMO_TOKEN` | 400 | Required EpsimoAI token not provided |

---

## Prerequisites Checklist

Before running enrichment, copyright, scoring, or FSD pipelines, these configuration records must exist (create via Tables API):

### UrlSettings (enrichment)

| Field | Description |
|-------|-------------|
| `companyUrl` | Company URL lookup service endpoint |
| `companyUrl_Apikey` | API key for company URL service |
| `emailFinder` | Email finder service endpoint |
| `emailFinder_Apikey` | API key for email finder |
| `enrichment1`–`enrichment10` | Enrichment service endpoints |
| `enrichment1_Apikey`–`enrichment10_Apikey` | Corresponding API keys |

### AgentSettings (copyright / AI content)

| Field | Description |
|-------|-------------|
| `projectId` | EpsimoAI project ID |
| `enrichment1AgentId`–`enrichment10AgentId` | Agent IDs per copyright process |

### SdrAiSettings (scoring)

| Field | Description |
|-------|-------------|
| `projectId` | EpsimoAI project ID |
| `aiLeadScoreAgentId` | Lead scoring agent |
| `aiQualificationAgentId` | Qualification assessment agent |
| `aiNextActionAgentId` | Next-action recommendation agent |
| `aiColdEmailAgentId` | Cold email generation agent |
| `aiInterestAgentId` | Interest analysis agent |
| `aiLinkedinConnectAgentId` | LinkedIn connect message agent |
| `aiCompetitorAnalysisAgentId` | Competitor analysis agent |
| `aiEngagementLevelAgentId` | Engagement level agent |
| `aiPurchaseWindowAgentId` | Purchase window estimation agent |
| `aiDecisionMakerRoleAgentId` | Decision-maker role agent |
| `aiSentimentAgentId` | Sentiment analysis agent |
| `aiSocialEngagementAgentId` | Social engagement agent |
| `aiNurturingStageAgentId` | Nurturing stage agent |
| `aiBudgetEstimationAgentId` | Budget estimation agent |
| `aiRiskScoreAgentId` | Risk scoring agent |
| `aiProductFitScoreAgentId` | Product-fit scoring agent |

### ICP (lead generation)

| Field | Description |
|-------|-------------|
| `apifyActorId` | Apify actor ID (required) |
| `apifyInput` | Actor input config (JSON string) |
| `maxLeads` | Max leads per run (default 100) |
| `client_id` | Client partition |

### EmailPlatformSettings (delivery)

| Field | Description |
|-------|-------------|
| `platform` | Platform name (e.g., `woodpecker`) |
| `apiKey` | Platform API key |
| `campaignId` | Default campaign ID |

---

## Reference Files

| File | Description |
|------|-------------|
| [SKILL.md](SKILL.md) | Skill metadata, prerequisites checklist, and quick-reference maps |
| [references/api_endpoints.md](references/api_endpoints.md) | Complete request/response schemas for every API endpoint |
| [references/cli_reference.md](references/cli_reference.md) | Full CLI command reference with syntax, flags, and examples |
| [references/workflows.md](references/workflows.md) | Step-by-step operational workflows (12 workflows) |
| [references/use_cases.md](references/use_cases.md) | End-to-end business scenario guides (3+ use cases) |

---

## License

See [LICENSE](LICENSE).


path: package.json
content: {
  "name": "leadgenius-cli-skill",
  "version": "1.1.0",
  "description": "AI agent skill for operating the LeadGenius Pro Automation API and lgp CLI. Covers ICP management, FSD pipeline automation, lead generation/enrichment/scoring, user provisioning, territory analysis, webhook management, and email platform integration.",
  "keywords": [
    "ai-skill",
    "agent-skill",
    "leadgenius",
    "b2b-leads",
    "lead-enrichment",
    "lead-scoring",
    "automation-api",
    "cli",
    "fsd-pipeline",
    "icp",
    "sales-intelligence",
    "sdr-ai",
    "claude-skill",
    "ai-agent"
  ],
  "homepage": "https://github.com/thierryteisseire/leadgenius-cli#readme",
  "repository": {
    "type": "git",
    "url": "git+https://github.com/thierryteisseire/leadgenius-cli.git"
  },
  "license": "SEE LICENSE IN LICENSE",
  "author": "Thierry Teisseire",
  "files": [
    "SKILL.md",
    "README.md",
    "references/"
  ],
  "leadgenius": {
    "requiredEnvVars": {
      "LGP_API_KEY": {
        "description": "LeadGenius Pro API key (lgp_ prefix). Created via POST /api/automation/users/provision. Required for all API and CLI operations.",
        "required": true
      },
      "LGP_URL": {
        "description": "Base URL of the LeadGenius Pro API.",
        "required": false,
        "default": "http://localhost:3000"
      },
      "LGP_ADMIN_KEY": {
        "description": "Admin key to bypass rate limits. Sent as X-Admin-Key header alongside X-API-Key. Use only for admin operations — grants elevated access.",
        "required": false
      }
    },
    "cliNotIncluded": true,
    "cliSource": "The lgp CLI (lgp.py) is part of the LeadGenius Pro application repository, not this skill package. This skill provides documentation only — obtain the CLI from your LeadGenius Pro deployment."
  }
}


path: LICENSE.txt
content: MIT License

Copyright (c) 2026 Thierry Teisseire

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.


path: references/workflows.md
content: # Workflow Reference

Step-by-step operational workflows for common multi-step processes in LeadGenius Pro.

---

## 1. Lead Deduplication Workflow

Find and merge duplicate leads within a client using configurable match fields.

### When to Use

- After a bulk import that may have introduced duplicates
- Periodic data hygiene on a client's lead set
- Before transferring leads to another client (clean source first)

### Steps

**Step 1 — Run a dry-run deduplication scan**

Identify duplicate groups without making changes. Choose one or more match fields based on desired confidence level.

| Match Field | Confidence | Description |
|-------------|------------|-------------|
| `email` | high | Exact email match (case-insensitive) |
| `linkedinUrl` | medium | Exact LinkedIn URL match |
| `fullName+companyName` | low | Combined name + company match |

**API:**

```bash
curl -s -X POST -H "X-[REDACTED_SECRET]" \
  -H "Content-Type: application/json" \
  -d '{
    "client_id": "YOUR_CLIENT_ID",
    "matchFields": ["email", "linkedinUrl", "fullName+companyName"],
    "dryRun": true
  }' \
  https://api.leadgenius.app/api/automation/leads/deduplicate | jq
```

**CLI:**

```bash
python lgp.py leads dedup --client YOUR_CLIENT_ID --match email,linkedinUrl,fullName+companyName
```

**Expected response:** A list of `matches`, each containing `matchField`, `confidence`, `matchValue`, and `leadIds` (the group of duplicates). Also returns `totalLeadsScanned` and `totalDuplicateGroups`.

**Step 2 — Review duplicate groups**

Examine each duplicate group returned in Step 1. For each group:

1. Note the `matchField` and `confidence` level.
2. Retrieve full details for each lead in the group to decide which to keep.

**API (for each lead in a group):**

```bash
curl -s -H "X-[REDACTED_SECRET]" \
  "https://api.leadgenius.app/api/automation/leads/LEAD_ID" | jq
```

**CLI:**

```bash
python lgp.py leads get LEAD_ID
```

Compare fields across leads in the group. Pick the lead with the most complete data as the "keep" lead.

**Step 3 — Resolve (merge) duplicates**

Merge data from duplicate leads into the chosen "keep" lead. Empty fields on the keep lead are filled from merge leads. Merge leads are marked `status: 'duplicate'`.

**API:**

```bash
curl -s -X POST -H "X-[REDACTED_SECRET]" \
  -H "Content-Type: application/json" \
  -d '{
    "keepLeadId": "KEEP_LEAD_ID",
    "mergeLeadIds": ["DUPLICATE_1", "DUPLICATE_2"]
  }' \
  https://api.leadgenius.app/api/automation/leads/deduplicate/resolve | jq
```

**CLI:**

```bash
python lgp.py leads dedup-resolve --keep KEEP_LEAD_ID --merge DUPLICATE_1,DUPLICATE_2
```

**Expected response:** Returns `mergedFields` (fields that were filled in), `conflicts` (fields where both keep and merge leads had values — keep lead's value wins), and `mergeLeadsMarked` count.

**Step 4 — Verify the merge result**

Retrieve the keep lead to confirm merged fields are populated correctly.

```bash
python lgp.py leads get KEEP_LEAD_ID
```

### Merge Rules

- Only empty fields on the keep lead are filled from merge leads.
- System fields (`id`, `owner`, `company_id`, `client_id`, `createdAt`, `updatedAt`) are never merged.
- If multiple merge leads have a value for the same empty field, the first merge lead's value wins.
- Conflicts (both keep and merge have a value) are reported but the keep lead's value is preserved.

### Common Failure Points and Recovery

| Failure | Cause | Recovery |
|---------|-------|----------|
| `NOT_FOUND` on resolve | One of the lead IDs is invalid or belongs to a different company | Verify lead IDs with `leads get` before resolving |
| No duplicate groups found | Match fields too strict, or data is already clean | Try broader match fields (e.g., add `fullName+companyName`) |
| Unexpected conflicts | Both leads have different values for the same field | Review conflicts in the response; manually update the keep lead if the merge lead's value is preferred |
| Large number of groups | Broad match fields on a large dataset | Process groups in batches; start with `email` (high confidence) first, then lower confidence fields |

---

## 2. Lead Transfer Workflow

Transfer leads between clients within the same company, with built-in duplicate detection against the target client.

### When to Use

- Reorganizing leads across campaigns or market segments
- Consolidating leads from a staging client into a production client
- Splitting a client's leads into multiple clients for team assignment

### Steps

**Step 1 — Dry-run the transfer**

Simulate the transfer to see how many leads will move and how many will be skipped as duplicates in the target client.

**API:**

```bash
curl -s -X POST -H "X-[REDACTED_SECRET]" \
  -H "Content-Type: application/json" \
  -d '{
    "fromClientId": "SOURCE_CLIENT_ID",
    "toClientId": "TARGET_CLIENT_ID",
    "leadIds": ["lead-1", "lead-2", "lead-3"],
    "dryRun": true
  }' \
  https://api.leadgenius.app/api/automation/leads/transfer | jq
```

**CLI:**

```bash
python lgp.py leads transfer --from SOURCE_CLIENT_ID --to TARGET_CLIENT_ID --all --dry-run
```

**Expected response:** Returns `transferred` count, `skippedDuplicates` count, and `details` with per-lead breakdown including which duplicates were detected and by which match field.

**Step 2 — Review dry-run results**

Check the response for:
- `transferred`: Number of leads that will move.
- `skippedDuplicates`: Leads already present in the target client (matched by email or linkedinUrl). Review the `details.skippedDuplicates` array to see which leads and match fields triggered the skip.

If too many duplicates are skipped, consider deduplicating the target client first (see Workflow 1).

**Step 3 — Execute the transfer**

Run the transfer for real by setting `dryRun: false` (or omitting it).

**API — Transfer specific leads:**

```bash
curl -s -X POST -H "X-[REDACTED_SECRET]" \
  -H "Content-Type: application/json" \
  -d '{
    "fromClientId": "SOURCE_CLIENT_ID",
    "toClientId": "TARGET_CLIENT_ID",
    "leadIds": ["lead-1", "lead-2", "lead-3"],
    "dryRun": false
  }' \
  https://api.leadgenius.app/api/automation/leads/transfer | jq
```

**API — Transfer all leads:**

```bash
curl -s -X POST -H "X-[REDACTED_SECRET]" \
  -H "Content-Type: application/json" \
  -d '{
    "fromClientId": "SOURCE_CLIENT_ID",
    "toClientId": "TARGET_CLIENT_ID",
    "transferAll": true,
    "dryRun": false
  }' \
  https://api.leadgenius.app/api/automation/leads/transfer | jq
```

**CLI:**

```bash
# Transfer all leads
python lgp.py leads transfer --from SOURCE_CLIENT_ID --to TARGET_CLIENT_ID --all

# Transfer specific leads (use API with leadIds array)
```

**Step 4 — Verify the transfer**

List leads in the target client to confirm they arrived.

```bash
python lgp.py leads list --client TARGET_CLIENT_ID --limit 10
```

Optionally, list the source client to confirm leads were moved out.

```bash
python lgp.py leads list --client SOURCE_CLIENT_ID --limit 10
```

### Transfer Rules

- Both source and target clients must belong to your company.
- Source and target must be different clients.
- Duplicate detection runs against the target client by email and linkedinUrl.
- Duplicates are skipped (not overwritten) — the target client's existing lead is preserved.
- Only the `client_id` field is updated on transferred leads; all other data is preserved.

### Common Failure Points and Recovery

| Failure | Cause | Recovery |
|---------|-------|----------|
| `CLIENT_WRONG_COMPANY` | Source or target client belongs to a different company | Verify both client IDs belong to your company with `leads list --client CLIENT_ID` |
| Source equals target | Same client ID for both `fromClientId` and `toClientId` | Use different client IDs |
| All leads skipped as duplicates | Target client already has all the same leads | Deduplicate the target client first, or verify you're using the correct client IDs |
| `NOT_FOUND` | Invalid client ID | List your clients to find valid IDs |
| Partial transfer errors | Individual lead update failures | Check the `errors` array in the response; retry failed leads individually |

---

## 3. Webhook Reprocessing Workflow

Re-match unmatched webhook events to leads. Useful when new leads have been imported since the original webhook was received.

### When to Use

- After importing new leads that should match previously unmatched webhook events
- When webhook events arrived before their corresponding leads were created
- Periodic cleanup of unmatched webhook events to improve engagement tracking

### Steps

**Step 1 — List unmatched webhook events**

Filter webhook events by match status to find events that didn't match any lead.

**API:**

```bash
curl -s -H "X-[REDACTED_SECRET]" \
  "https://api.leadgenius.app/api/automation/webhook-events?matchStatus=unmatched&limit=50" | jq
```

**CLI:**

```bash
python lgp.py webhooks list --platform woodpecker --limit 50
```

Review the results. Each event includes `normalizedData` with contact information (email, name, LinkedIn URL) that will be used for matching.

**Step 2 — Reprocess individual events**

For each unmatched event, trigger reprocessing. The system re-runs lead matching using the following priority:

| Priority | Match Method | Confidence | GSI Used |
|----------|-------------|------------|----------|
| 1 | Email | high | `email-index` |
| 2 | LinkedIn URL | medium | `company_id` GSI + filter |
| 3 | First name + Last name | low | `firstName-lastName-index` |

**API:**

```bash
curl -s -X POST -H "X-[REDACTED_SECRET]" \
  https://api.leadgenius.app/api/automation/webhook-events/EVENT_ID/reprocess | jq
```

**CLI:**

```bash
python lgp.py webhooks reprocess EVENT_ID
```

**Expected response:** Returns updated `matchStatus`, `matchedLeadId`, `matchConfidence`, and `matchMethod`. If a lead is matched, the webhook event is appended to the lead's `engagementHistory` and `engagementScore` is recalculated.

**Step 3 — Batch reprocess multiple events**

To reprocess multiple unmatched events, iterate over the event IDs from Step 1.

**Bash loop example:**

```bash
for EVENT_ID in event-1 event-2 event-3; do
  curl -s -X POST -H "X-[REDACTED_SECRET]" \
    https://api.leadgenius.app/api/automation/webhook-events/$EVENT_ID/reprocess | jq
done
```

**CLI loop:**

```bash
for EVENT_ID in event-1 event-2 event-3; do
  python lgp.py webhooks reprocess $EVENT_ID
done
```

**Step 4 — Verify match results**

After reprocessing, list webhook events again to check how many are now matched.

```bash
curl -s -H "X-[REDACTED_SECRET]" \
  "https://api.leadgenius.app/api/automation/webhook-events?matchStatus=matched&limit=50" | jq
```

For matched events, verify the engagement was recorded on the lead:

```bash
curl -s -H "X-[REDACTED_SECRET]" \
  "https://api.leadgenius.app/api/automation/leads/MATCHED_LEAD_ID/activities" | jq
```

**CLI:**

```bash
python lgp.py webhooks list --limit 50
```

**Step 5 — Clean up permanently unmatched events (optional)**

If events remain unmatched after reprocessing and are no longer needed, delete them.

**API:**

```bash
curl -s -X DELETE -H "X-[REDACTED_SECRET]" \
  -H "Content-Type: application/json" \
  -d '{"eventIds": ["event-1", "event-2"]}' \
  https://api.leadgenius.app/api/automation/webhook-events | jq
```

### Matching Behavior

- Matching uses the `normalizedData` JSON field from the webhook event, which contains extracted contact information (email, name, LinkedIn URL).
- The system tries each match method in priority order and stops at the first match.
- If a lead is matched, the webhook event type (e.g., `email_opened`) is appended to the lead's `engagementHistory` and `engagementScore` is recalculated.
- The WebhookLog record is updated with `matchStatus`, `matchedLeadId`, and `matchConfidence`.

### Common Failure Points and Recovery

| Failure | Cause | Recovery |
|---------|-------|----------|
| Event still unmatched after reprocess | No lead in the system matches the event's contact data | Import the missing lead first, then reprocess again |
| `NOT_FOUND` on reprocess | Invalid event ID | List events to get valid IDs |
| Low-confidence match (name only) | Email and LinkedIn URL not available in the webhook data | Verify the match manually by comparing the matched lead with the event's `normalizedData` |
| Engagement score not updated | Lead was matched but activity logging failed | Check the lead's activities endpoint; manually log the activity if missing |
| Duplicate engagement entries | Event was reprocessed multiple times | Check the lead's activity history; the system should deduplicate by `eventHash`, but verify |


---

## 4. Territory Analysis Workflow

Build and maintain company-level intelligence from lead data. Aggregate leads into TerritoryCompany records, run content analysis, generate events, and view the radar dashboard.

### When to Use

- After importing or enriching a batch of leads to build company-level insights
- Periodically refreshing territory intelligence for sales planning
- Before territory review meetings to surface recent company activity
- When onboarding a new client and building initial territory maps

### Steps

**Step 1 — Aggregate companies from lead data**

Trigger aggregation to create or update TerritoryCompany records from EnrichLeads. Each unique `companyName` within a client becomes a TerritoryCompany with metrics like `totalLeads`, `qualifiedLeads`, and `averageLeadScore`.

**API:**

```bash
curl -s -X POST -H "X-[REDACTED_SECRET]" \
  -H "Content-Type: application/json" \
  -d '{
    "client_id": "YOUR_CLIENT_ID",
    "forceRefresh": false,
    "maxLeads": 1000
  }' \
  https://api.leadgenius.app/api/automation/companies/aggregate | jq
```

To aggregate a single company only:

```bash
curl -s -X POST -H "X-[REDACTED_SECRET]" \
  -H "Content-Type: application/json" \
  -d '{
    "client_id": "YOUR_CLIENT_ID",
    "companyName": "Acme Corp",
    "forceRefresh": true
  }' \
  https://api.leadgenius.app/api/automation/companies/aggregate | jq
```

**Expected response:** Returns `companiesCreated`, `companiesUpdated`, `totalLeadsProcessed`, and any `errors`.

Use `forceRefresh: true` to delete and re-create existing TerritoryCompany records (useful when lead data has changed significantly).

**Step 2 — List territory companies**

Browse the aggregated companies with optional filtering and sorting.

**API:**

```bash
curl -s -H "X-[REDACTED_SECRET]" \
  "https://api.leadgenius.app/api/automation/companies?client_id=YOUR_CLIENT_ID&sortBy=totalLeads&limit=50" | jq
```

**CLI:**

```bash
python lgp.py companies list --client YOUR_CLIENT_ID
python lgp.py companies list --client YOUR_CLIENT_ID --sort totalLeads
```

Available `sortBy` values: `totalLeads`, `qualifiedLeads`, `averageLeadScore`, `lastActivityDate`, `companyName`.

Optional filters: `industry`, `country`, `minLeads`, `maxLeads`, `minScore`, `maxScore`.

**Step 3 — View company detail and associated leads**

Get a complete TerritoryCompany record including content analysis fields and a summary of associated leads.

**API:**

```bash
curl -s -H "X-[REDACTED_SECRET]" \
  "https://api.leadgenius.app/api/automation/companies/COMPANY_ID" | jq
```

**CLI:**

```bash
python lgp.py companies get COMPANY_ID
```

To retrieve the full lead records associated with a company:

```bash
curl -s -H "X-[REDACTED_SECRET]" \
  "https://api.leadgenius.app/api/automation/companies/COMPANY_ID/leads?limit=50" | jq
```

**Step 4 — Run content analysis**

Re-run content analysis on a TerritoryCompany to extract insights from its associated leads. This updates fields like `contentTopics`, `contentKeywords`, `painPoints`, `valuePropositions`, `competitorMentions`, `engagementInsights`, and `contentRecommendations`.

**API:**

```bash
curl -s -X POST -H "X-[REDACTED_SECRET]" \
  https://api.leadgenius.app/api/automation/companies/COMPANY_ID/content-analysis | jq
```

**Expected response:** The updated TerritoryCompany record with all content analysis fields populated and `lastContentAnalysisDate` set.

**Step 5 — Generate events from lead activity**

Auto-generate CompanyEvent records from recent lead activity. Events are created for new leads, qualified leads, score increases, and new companies.

**API:**

```bash
curl -s -X POST -H "X-[REDACTED_SECRET]" \
  -H "Content-Type: application/json" \
  -d '{
    "client_id": "YOUR_CLIENT_ID",
    "since": "2025-01-01T00:00:00Z",
    "maxLeads": 500
  }' \
  https://api.leadgenius.app/api/automation/companies/events/generate | jq
```

**Expected response:** Returns `eventsCreated` and `eventsByType` breakdown (e.g., `{"new_lead": 15, "lead_qualified": 10}`).

You can also create manual events for custom intelligence:

```bash
curl -s -X POST -H "X-[REDACTED_SECRET]" \
  -H "Content-Type: application/json" \
  -d '{
    "territoryCompanyId": "COMPANY_ID",
    "eventType": "custom",
    "eventTitle": "Partnership announced",
    "eventDescription": "Acme announced partnership with Beta Inc",
    "eventData": {"source": "press_release"}
  }' \
  https://api.leadgenius.app/api/automation/companies/events | jq
```

**Step 6 — View the radar dashboard**

Get a summary view of recent events, event counts by type, top active companies, and a timeline.

**API:**

```bash
curl -s -H "X-[REDACTED_SECRET]" \
  "https://api.leadgenius.app/api/automation/companies/events/radar?client_id=YOUR_CLIENT_ID" | jq
```

**Expected response:** Returns `recentEvents`, `eventCountsByType`, `topActiveCompanies`, and `timeline`.

**Step 7 — Filter and review events (optional)**

List events with filters to focus on specific activity.

**API:**

```bash
# Filter by event type
curl -s -H "X-[REDACTED_SECRET]" \
  "https://api.leadgenius.app/api/automation/companies/events?client_id=YOUR_CLIENT_ID&eventType=lead_qualified" | jq

# Filter by company
curl -s -H "X-[REDACTED_SECRET]" \
  "https://api.leadgenius.app/api/automation/companies/events?client_id=YOUR_CLIENT_ID&territoryCompanyId=COMPANY_ID" | jq

# Filter by date range
curl -s -H "X-[REDACTED_SECRET]" \
  "https://api.leadgenius.app/api/automation/companies/events?client_id=YOUR_CLIENT_ID&dateFrom=2025-06-01T00:00:00Z&dateTo=2025-06-30T23:59:59Z" | jq
```

Valid `eventType` values: `new_lead`, `lead_qualified`, `score_increased`, `new_company`, `custom`.

### Common Failure Points and Recovery

| Failure | Cause | Recovery |
|---------|-------|----------|
| `MISSING_CLIENT_ID` on aggregate | No `client_id` in request body | Include `client_id` in the request body |
| Zero companies created | No leads exist for the client, or all companies already aggregated | Check lead count with `leads list --client CLIENT_ID`; use `forceRefresh: true` to re-aggregate |
| Content analysis returns empty fields | Company has very few leads or leads lack enrichment data | Enrich leads first (see Tasks API), then re-run content analysis |
| Event generation returns zero events | No recent lead activity since the `since` date | Adjust the `since` parameter to an earlier date, or omit it to process all leads |
| `NOT_FOUND` on company detail | Invalid TerritoryCompany ID | List companies first to get valid IDs |

---

## 5. Engagement Tracking Workflow

Log engagement activities on leads, retrieve engagement history, and understand how the engagement score is calculated.

### When to Use

- Tracking outreach activities (emails, calls, LinkedIn messages) on leads
- Building engagement history for lead scoring and prioritization
- Auditing engagement data before running SDR AI scoring
- Monitoring campaign effectiveness through activity patterns

### Steps

**Step 1 — Log a single activity on a lead**

Record an engagement activity with a type, optional notes, and optional metadata.

**API:**

```bash
curl -s -X POST -H "X-[REDACTED_SECRET]" \
  -H "Content-Type: application/json" \
  -d '{
    "type": "email_opened",
    "notes": "Opened Q1 campaign email",
    "metadata": {"campaignId": "camp-1"}
  }' \
  https://api.leadgenius.app/api/automation/leads/LEAD_ID/activities | jq
```

**CLI:**

```bash
python lgp.py leads activity LEAD_ID --type email_opened --notes "Opened Q1 campaign email"
```

**Expected response:** Returns `activitiesAdded`, `totalActivities`, updated `engagementScore`, and `lastEngagementAt`.

**Step 2 — Log multiple activities in batch**

Record several activities at once, each with its own timestamp.

**API:**

```bash
curl -s -X POST -H "X-[REDACTED_SECRET]" \
  -H "Content-Type: application/json" \
  -d '{
    "activities": [
      {"type": "email_sent", "timestamp": "2026-03-01T09:00:00.000Z"},
      {"type": "email_opened", "timestamp": "2026-03-01T10:00:00.000Z"},
      {"type": "email_clicked", "timestamp": "2026-03-01T10:05:00.000Z"},
      {"type": "meeting_scheduled", "timestamp": "2026-03-02T14:00:00.000Z", "notes": "Demo call booked"}
    ]
  }' \
  https://api.leadgenius.app/api/automation/leads/LEAD_ID/activities | jq
```

**Step 3 — Retrieve engagement history**

Get the full activity history and current engagement score for a lead.

**API:**

```bash
curl -s -H "X-[REDACTED_SECRET]" \
  "https://api.leadgenius.app/api/automation/leads/LEAD_ID/activities" | jq
```

**Expected response:** Returns `leadId`, `totalActivities`, `engagementScore`, `lastEngagementAt`, and an `activities` array with each activity's `type`, `timestamp`, `notes`, and `metadata`.

**Step 4 — Understand the engagement score**

The engagement score is automatically recalculated each time activities are logged. It uses weighted activity types with time decay.

**Activity type weights:**

| Activity | Points |
|----------|--------|
| `contract_signed`, `meeting_completed` | 30 |
| `meeting_scheduled`, `demo_requested` | 25 |
| `email_answered`, `proposal_sent`, `form_submitted` | 20 |
| `linkedin_message_received`, `call_completed` | 15 |
| `linkedin_connection_accepted`, `content_downloaded` | 10 |
| `email_clicked` | 8 |
| `email_opened`, `custom` | 5 |
| `linkedin_message_sent`, `email_sent`, `website_visited` | 2–3 |
| `linkedin_connection_sent`, `linkedin_profile_viewed`, `call_no_answer` | 1–2 |
| `email_bounced` | -5 |

**Time decay:** Activities less than 30 days old receive full weight. Activities older than 30 days receive 50% weight.

**Score cap:** The engagement score is capped at 100.

**Valid activity types:** `linkedin_connection_sent`, `linkedin_connection_accepted`, `linkedin_message_sent`, `linkedin_message_received`, `linkedin_profile_viewed`, `email_sent`, `email_opened`, `email_clicked`, `email_answered`, `email_bounced`, `call_completed`, `call_no_answer`, `meeting_scheduled`, `meeting_completed`, `form_submitted`, `website_visited`, `content_downloaded`, `demo_requested`, `proposal_sent`, `contract_signed`, `custom`.

**Step 5 — Verify engagement on the lead record**

Retrieve the full lead to confirm engagement fields are updated.

**API:**

```bash
curl -s -H "X-[REDACTED_SECRET]" \
  "https://api.leadgenius.app/api/automation/leads/LEAD_ID" | jq '.data | {engagementScore, lastEngagementAt, engagementHistory}'
```

**CLI:**

```bash
python lgp.py leads get LEAD_ID
```

Check that `engagementScore`, `lastEngagementAt`, and `engagementHistory` reflect the logged activities.

### Common Failure Points and Recovery

| Failure | Cause | Recovery |
|---------|-------|----------|
| `NOT_FOUND` on activity log | Invalid lead ID | Verify the lead ID with `leads get LEAD_ID` |
| Invalid activity `type` | Unrecognized activity type string | Use one of the valid activity types listed above |
| Score not increasing | Activities are older than 30 days (50% weight) or low-point types | Log higher-weight activities or more recent ones |
| Score stuck at 100 | Cap reached | Score is capped at 100; additional activities won't increase it further |
| Duplicate activities | Same activity logged multiple times | Review the activities list; the system does not deduplicate manual activity logs |
| `engagementHistory` empty after logging | Activity was logged but lead record not refreshed | Re-fetch the lead; if still empty, check the activity endpoint directly |

---

## 6. Data Health Check Workflow

Validate lead ownership, detect orphaned records, and verify client-company relationships across your data.

### When to Use

- After bulk imports or transfers to verify data integrity
- Periodic data hygiene audits
- Before running enrichment or scoring pipelines to ensure clean input data
- When leads appear to be missing or inaccessible

### Steps

**Step 1 — Run ownership validation**

Scan all EnrichLeads for your company and report ownership issues. The validation checks for orphaned leads, company mismatches, and null owners.

**API:**

```bash
curl -s -X POST -H "X-[REDACTED_SECRET]" \
  https://api.leadgenius.app/api/automation/leads/validate-ownership | jq
```

**CLI:**

```bash
python lgp.py leads validate-ownership
```

**Expected response:**

```json
{
  "success": true,
  "data": {
    "status": "healthy",
    "totalLeadsScanned": 200,
    "orphanedRecords": 0,
    "mismatchedCompany": 0,
    "nullOwner": 0,
    "totalIssues": 0,
    "details": []
  }
}
```

**Checks performed:**

| Check | Description |
|-------|-------------|
| Orphaned records | Lead's `client_id` references a non-existent Client record |
| Mismatched company | Client's `company_id` doesn't match the lead's `company_id` |
| Null owner | Lead has an empty or null `owner` field |

If `status` is `"healthy"` and `totalIssues` is 0, your data is clean. Otherwise, proceed to the next steps.

**Step 2 — Review issue details**

When issues are found, the `details` array contains specifics about each problem. Review the details to understand the scope.

```bash
curl -s -X POST -H "X-[REDACTED_SECRET]" \
  https://api.leadgenius.app/api/automation/leads/validate-ownership | jq '.data.details'
```

Each detail entry includes the lead ID, the issue type, and relevant field values to help diagnose the problem.

**Step 3 — Fix orphaned records**

Orphaned leads reference a `client_id` that no longer exists. To fix:

1. List your available clients to find a valid target.

```bash
python lgp.py tables list Client
```

2. Transfer orphaned leads to a valid client using the transfer endpoint.

```bash
curl -s -X POST -H "X-[REDACTED_SECRET]" \
  -H "Content-Type: application/json" \
  -d '{
    "fromClientId": "ORPHANED_CLIENT_ID",
    "toClientId": "VALID_CLIENT_ID",
    "transferAll": true,
    "dryRun": false
  }' \
  https://api.leadgenius.app/api/automation/leads/transfer | jq
```

Alternatively, if the client was deleted accidentally, re-create it:

```bash
curl -s -X POST -H "X-[REDACTED_SECRET]" \
  -H "Content-Type: application/json" \
  -d '{"name": "Restored Client", "company_id": "YOUR_COMPANY_ID"}' \
  https://api.leadgenius.app/api/automation/tables/Client | jq
```

**Step 4 — Verify client-company relationships**

Confirm that each client belongs to the correct company by listing clients and checking their `company_id`.

```bash
python lgp.py tables list Client
```

Cross-reference with your company ID. If a client has a mismatched `company_id`, leads assigned to it will show as `mismatchedCompany` issues.

**Step 5 — Re-run validation to confirm fixes**

After resolving issues, run the ownership validation again to confirm all problems are resolved.

**API:**

```bash
curl -s -X POST -H "X-[REDACTED_SECRET]" \
  https://api.leadgenius.app/api/automation/leads/validate-ownership | jq
```

**CLI:**

```bash
python lgp.py leads validate-ownership
```

Confirm `status` is `"healthy"` and `totalIssues` is 0.

### Common Failure Points and Recovery

| Failure | Cause | Recovery |
|---------|-------|----------|
| Large number of orphaned records | Client was deleted while leads still referenced it | Re-create the client or transfer leads to an existing client |
| Mismatched company IDs | Leads were imported with incorrect `company_id` (should be auto-set) | This typically indicates a data migration issue; contact support or re-import leads |
| Null owner records | Leads created through a non-standard path that bypassed owner assignment | Re-import the affected leads through the standard import endpoint, which auto-sets `owner` |
| Validation times out | Very large dataset (thousands of leads) | The endpoint scans all leads for your company; if it times out, try during off-peak hours or contact support |
| Transfer fails for orphaned leads | The orphaned `client_id` can't be used as `fromClientId` | Use the lead search endpoint to find orphaned leads by ID, then import them fresh into the target client |


---

## 7. ICP-First Campaign Setup Workflow

Define an Ideal Customer Profile, validate its Apify configuration, create an FSD campaign, run the pipeline, and verify results end-to-end.

### When to Use

- Launching a new lead generation campaign targeting a specific market segment
- Setting up ICP-driven automation for the first time
- Creating a repeatable campaign template from a well-defined ICP

### Steps

**Step 1 — Define the ICP with targeting criteria and Apify config**

Create an ICP record with all targeting criteria and the Apify actor configuration for lead generation.

**API:**

```bash
curl -s -X POST -H "X-[REDACTED_SECRET]" \
  -H "Content-Type: application/json" \
  -d '{
    "client_id": "YOUR_CLIENT_ID",
    "name": "Enterprise SaaS Decision Makers",
    "description": "VP+ at SaaS companies, 200-1000 employees, US/UK",
    "industries": ["Software", "SaaS", "Cloud Computing"],
    "companySizes": ["201-500", "501-1000"],
    "geographies": ["United States", "United Kingdom"],
    "jobTitles": ["VP Sales", "VP Marketing", "CRO", "CMO"],
    "seniority": ["VP", "C-Suite", "Director"],
    "departments": ["Sales", "Marketing"],
    "keywords": ["B2B", "enterprise sales", "demand generation"],
    "technologies": ["Salesforce", "HubSpot", "Outreach"],
    "apifyActorId": "apify/linkedin-sales-navigator",
    "apifyInput": "{\"searchUrl\":\"https://www.linkedin.com/sales/search/people?query=...\",\"maxResults\":200}",
    "maxLeads": 200,
    "qualificationCriteria": "{\"minScore\":60,\"requiredFields\":[\"email\",\"companyName\"]}",
    "scoringWeights": "{\"industry\":0.3,\"seniority\":0.3,\"companySize\":0.2,\"technology\":0.2}"
  }' \
  https://api.leadgenius.app/api/automation/tables/ICP | jq
```

**CLI:**

```bash
python lgp.py tables create ICP --data '{
  "client_id": "YOUR_CLIENT_ID",
  "name": "Enterprise SaaS Decision Makers",
  "industries": ["Software", "SaaS"],
  "companySizes": ["201-500", "501-1000"],
  "geographies": ["United States", "United Kingdom"],
  "jobTitles": ["VP Sales", "VP Marketing", "CRO"],
  "seniority": ["VP", "C-Suite"],
  "apifyActorId": "apify/linkedin-sales-navigator",
  "apifyInput": "{\"searchUrl\":\"https://www.linkedin.com/sales/search/people?query=...\"}",
  "maxLeads": 200
}'
```

Save the returned `id` as `ICP_ID`.

**Step 2 — Validate the ICP has Apify configuration**

Retrieve the ICP and confirm `apifyActorId` is set. Without it, the FSD pipeline cannot generate leads.

**API:**

```bash
curl -s -H "X-[REDACTED_SECRET]" \
  "https://api.leadgenius.app/api/automation/tables/ICP/ICP_ID" | jq '{apifyActorId, apifyInput, maxLeads}'
```

**CLI:**

```bash
python lgp.py tables get ICP ICP_ID
```

Verify that `apifyActorId` is non-null. If missing, the pipeline run will fail with `ICP_NO_APIFY`.

**Step 3 — Create an FSD campaign linked to the ICP**

Create a campaign that references the ICP. The pipeline will auto-resolve `apifyActorId` and `apifyInput` from the ICP record.

**API:**

```bash
curl -s -X POST -H "X-[REDACTED_SECRET]" \
  -H "Content-Type: application/json" \
  -d '{
    "client_id": "YOUR_CLIENT_ID",
    "name": "Enterprise SaaS Q1 Campaign",
    "icpId": "ICP_ID",
    "frequency": "weekly",
    "targetLeadCount": 200,
    "enrichAfterGeneration": true,
    "scoreAfterEnrichment": true
  }' \
  https://api.leadgenius.app/api/automation/fsd/campaigns | jq
```

Save the returned campaign `id` as `CAMPAIGN_ID`.

**Step 4 — Run the FSD pipeline**

Trigger a pipeline run using the ICP. The API resolves `apifyActorId` and `apifyInput` from the ICP record automatically.

**API:**

```bash
curl -s -X POST -H "X-[REDACTED_SECRET]" \
  -H "Content-Type: application/json" \
  -d '{
    "client_id": "YOUR_CLIENT_ID",
    "icpId": "ICP_ID",
    "targetLeadCount": 200,
    "enrichAfterGeneration": true,
    "scoreAfterEnrichment": true,
    "campaignId": "CAMPAIGN_ID"
  }' \
  https://api.leadgenius.app/api/automation/fsd/run | jq
```

**CLI:**

```bash
python lgp.py fsd run --client YOUR_CLIENT_ID --icp ICP_ID --target 200
```

Save the returned `pipelineId` as `PIPELINE_ID`.

**Step 5 — Monitor pipeline stage progression**

Poll the pipeline status to track progress through each stage: `generating` → `enriching` → `scoring` → `completed`.

**API:**

```bash
curl -s -H "X-[REDACTED_SECRET]" \
  "https://api.leadgenius.app/api/automation/fsd/run/PIPELINE_ID" | jq '{stage, leadsGenerated, leadsEnriched, leadsScored, leadsQualified}'
```

**CLI:**

```bash
python lgp.py fsd status PIPELINE_ID
```

Repeat until `stage` is `completed` or `failed`.

**Step 6 — Verify generated leads**

List leads in the client to confirm they were generated and enriched.

**API:**

```bash
curl -s -H "X-[REDACTED_SECRET]" \
  "https://api.leadgenius.app/api/automation/leads?client_id=YOUR_CLIENT_ID&limit=20" | jq
```

**CLI:**

```bash
python lgp.py leads list --client YOUR_CLIENT_ID --limit 20
```

Check that lead count matches `leadsGenerated` from the pipeline status and that enrichment/scoring fields are populated.

### Common Failure Points and Recovery

| Failure | Cause | Recovery |
|---------|-------|----------|
| `ICP_NOT_FOUND` | Invalid `icpId` or ICP belongs to a different company | Verify ICP ID with `tables get ICP ICP_ID` |
| `ICP_NO_APIFY` | ICP record missing `apifyActorId` | Update the ICP: `tables update ICP ICP_ID --data '{"apifyActorId":"apify/actor-id"}'` |
| `ICP_LOOKUP_FAILED` | Internal error resolving ICP | Retry the pipeline run; if persistent, verify ICP record integrity |
| Pipeline stuck at `generating` | Apify actor is slow or the search URL returns many results | Wait longer; check Apify dashboard for actor run status |
| Pipeline `failed` at enrichment | UrlSettings not configured | Create UrlSettings via `tables create UrlSettings` with required service URLs |
| Zero leads generated | Apify search parameters too narrow or actor misconfigured | Update ICP `apifyInput` with broader search criteria |


---

## 8. Multi-ICP Campaign Strategy Workflow

Create multiple ICPs for different market segments, run separate FSD campaigns per ICP, and compare generation results across segments.

### When to Use

- Targeting multiple market segments simultaneously (e.g., Enterprise vs. Mid-Market)
- A/B testing different ICP definitions to find the best-performing profile
- Running parallel campaigns for different geographies or industries

### Steps

**Step 1 — Create ICPs for each market segment**

Define separate ICP records for each target segment with distinct targeting criteria.

**Segment A — Enterprise SaaS:**

```bash
curl -s -X POST -H "X-[REDACTED_SECRET]" \
  -H "Content-Type: application/json" \
  -d '{
    "client_id": "YOUR_CLIENT_ID",
    "name": "Enterprise SaaS",
    "industries": ["Software", "SaaS"],
    "companySizes": ["501-1000", "1001-5000"],
    "geographies": ["United States"],
    "jobTitles": ["VP Sales", "CRO", "Head of Revenue"],
    "seniority": ["VP", "C-Suite"],
    "apifyActorId": "apify/linkedin-sales-navigator",
    "apifyInput": "{\"searchUrl\":\"https://linkedin.com/sales/search/people?query=enterprise-saas\"}",
    "maxLeads": 150
  }' \
  https://api.leadgenius.app/api/automation/tables/ICP | jq
```

Save as `ICP_A_ID`.

**Segment B — Mid-Market FinTech:**

```bash
curl -s -X POST -H "X-[REDACTED_SECRET]" \
  -H "Content-Type: application/json" \
  -d '{
    "client_id": "YOUR_CLIENT_ID",
    "name": "Mid-Market FinTech",
    "industries": ["Financial Technology", "Banking", "Payments"],
    "companySizes": ["51-200", "201-500"],
    "geographies": ["United States", "Canada"],
    "jobTitles": ["Head of Growth", "VP Marketing", "Director of Sales"],
    "seniority": ["Director", "VP"],
    "apifyActorId": "apify/linkedin-sales-navigator",
    "apifyInput": "{\"searchUrl\":\"https://linkedin.com/sales/search/people?query=fintech-midmarket\"}",
    "maxLeads": 100
  }' \
  https://api.leadgenius.app/api/automation/tables/ICP | jq
```

Save as `ICP_B_ID`.

**CLI (for each):**

```bash
python lgp.py tables create ICP --data '{"client_id":"YOUR_CLIENT_ID","name":"Enterprise SaaS","industries":["Software","SaaS"],"companySizes":["501-1000"],"apifyActorId":"apify/linkedin-sales-navigator","apifyInput":"{\"searchUrl\":\"...\"}","maxLeads":150}'
```

**Step 2 — Create separate FSD campaigns per ICP**

Each campaign targets a different ICP so results can be tracked independently.

**Campaign A:**

```bash
curl -s -X POST -H "X-[REDACTED_SECRET]" \
  -H "Content-Type: application/json" \
  -d '{
    "client_id": "YOUR_CLIENT_ID",
    "name": "Enterprise SaaS Campaign",
    "icpId": "ICP_A_ID",
    "frequency": "weekly",
    "targetLeadCount": 150,
    "enrichAfterGeneration": true,
    "scoreAfterEnrichment": true
  }' \
  https://api.leadgenius.app/api/automation/fsd/campaigns | jq
```

**Campaign B:**

```bash
curl -s -X POST -H "X-[REDACTED_SECRET]" \
  -H "Content-Type: application/json" \
  -d '{
    "client_id": "YOUR_CLIENT_ID",
    "name": "Mid-Market FinTech Campaign",
    "icpId": "ICP_B_ID",
    "frequency": "weekly",
    "targetLeadCount": 100,
    "enrichAfterGeneration": true,
    "scoreAfterEnrichment": true
  }' \
  https://api.leadgenius.app/api/automation/fsd/campaigns | jq
```

Save campaign IDs as `CAMPAIGN_A_ID` and `CAMPAIGN_B_ID`.

**Step 3 — Run pipelines for each campaign**

Trigger pipeline runs for both campaigns.

```bash
# Campaign A
curl -s -X POST -H "X-[REDACTED_SECRET]" \
  -H "Content-Type: application/json" \
  -d '{"client_id":"YOUR_CLIENT_ID","icpId":"ICP_A_ID","targetLeadCount":150,"enrichAfterGeneration":true,"scoreAfterEnrichment":true,"campaignId":"CAMPAIGN_A_ID"}' \
  https://api.leadgenius.app/api/automation/fsd/run | jq

# Campaign B
curl -s -X POST -H "X-[REDACTED_SECRET]" \
  -H "Content-Type: application/json" \
  -d '{"client_id":"YOUR_CLIENT_ID","icpId":"ICP_B_ID","targetLeadCount":100,"enrichAfterGeneration":true,"scoreAfterEnrichment":true,"campaignId":"CAMPAIGN_B_ID"}' \
  https://api.leadgenius.app/api/automation/fsd/run | jq
```

**CLI:**

```bash
python lgp.py fsd run --client YOUR_CLIENT_ID --icp ICP_A_ID --target 150
python lgp.py fsd run --client YOUR_CLIENT_ID --icp ICP_B_ID --target 100
```

Save both `pipelineId` values.

**Step 4 — Monitor both pipelines**

Track progress for each pipeline independently.

```bash
python lgp.py fsd status PIPELINE_A_ID
python lgp.py fsd status PIPELINE_B_ID
```

**Step 5 — Compare results across ICPs**

Once both pipelines complete, compare the campaign metrics.

```bash
# Get Campaign A metrics
curl -s -H "X-[REDACTED_SECRET]" \
  "https://api.leadgenius.app/api/automation/fsd/campaigns/CAMPAIGN_A_ID" | jq '{name, totalLeadsGenerated, totalLeadsEnriched, totalLeadsScored}'

# Get Campaign B metrics
curl -s -H "X-[REDACTED_SECRET]" \
  "https://api.leadgenius.app/api/automation/fsd/campaigns/CAMPAIGN_B_ID" | jq '{name, totalLeadsGenerated, totalLeadsEnriched, totalLeadsScored}'
```

**CLI:**

```bash
python lgp.py fsd campaign CAMPAIGN_A_ID
python lgp.py fsd campaign CAMPAIGN_B_ID
```

Compare `totalLeadsGenerated`, `totalLeadsEnriched`, `totalLeadsScored`, and `totalLeadsSent` across campaigns to determine which ICP performs better.

**Step 6 — Deactivate underperforming ICPs (optional)**

If one segment underperforms, deactivate its ICP.

```bash
curl -s -X PUT -H "X-[REDACTED_SECRET]" \
  -H "Content-Type: application/json" \
  -d '{"isActive": false}' \
  https://api.leadgenius.app/api/automation/tables/ICP/ICP_B_ID | jq
```

**CLI:**

```bash
python lgp.py tables update ICP ICP_B_ID --data '{"isActive": false}'
```

### Common Failure Points and Recovery

| Failure | Cause | Recovery |
|---------|-------|----------|
| One pipeline fails, other succeeds | Different Apify configs or search parameters | Check the failed pipeline's `errorMessage`; fix the ICP's `apifyInput` and rerun |
| Both pipelines generate similar leads | ICP targeting criteria overlap too much | Narrow the criteria — use distinct `geographies`, `companySizes`, or `industries` per ICP |
| Campaign metrics show zero for one segment | ICP search parameters return no results from Apify | Broaden the ICP's `apifyInput` search URL or adjust `maxLeads` |
| Rate limiting on parallel runs | Two pipelines hitting Apify simultaneously | Stagger the runs by a few minutes; run one, wait for `generating` to complete, then run the other |


---

## 9. Iterative ICP Refinement Workflow

Run an initial ICP-based generation, review lead quality scores, update ICP targeting criteria based on results, run a refined generation, and compare before/after metrics.

### When to Use

- Initial ICP definition produced low-quality leads and needs tuning
- Optimizing an existing ICP to improve lead score averages
- Testing whether narrower or broader targeting criteria yield better results
- Continuous improvement of lead generation quality over time

### Steps

**Step 1 — Run an initial ICP-based generation**

Trigger a pipeline run with the current ICP to establish a baseline.

**API:**

```bash
curl -s -X POST -H "X-[REDACTED_SECRET]" \
  -H "Content-Type: application/json" \
  -d '{
    "client_id": "YOUR_CLIENT_ID",
    "icpId": "ICP_ID",
    "targetLeadCount": 100,
    "enrichAfterGeneration": true,
    "scoreAfterEnrichment": true
  }' \
  https://api.leadgenius.app/api/automation/fsd/run | jq
```

**CLI:**

```bash
python lgp.py fsd run --client YOUR_CLIENT_ID --icp ICP_ID --target 100
```

Save the `pipelineId` as `PIPELINE_V1_ID`. Wait for the pipeline to reach `completed`.

```bash
python lgp.py fsd status PIPELINE_V1_ID
```

**Step 2 — Review lead quality scores**

List the generated leads and examine their `aiLeadScore` and `aiQualification` values.

**API:**

```bash
curl -s -H "X-[REDACTED_SECRET]" \
  "https://api.leadgenius.app/api/automation/leads?client_id=YOUR_CLIENT_ID&limit=50" | jq '.data.items[] | {fullName, companyName, title, aiLeadScore, aiQualification}'
```

**CLI:**

```bash
python lgp.py leads list --client YOUR_CLIENT_ID --limit 50
```

Note the average `aiLeadScore`, the distribution of `aiQualification` values, and which leads scored lowest. Identify patterns — are low-scoring leads from the wrong industry, wrong seniority level, or wrong company size?

**Step 3 — Record baseline metrics from the pipeline run**

Capture the pipeline's per-stage metrics for comparison later.

```bash
curl -s -H "X-[REDACTED_SECRET]" \
  "https://api.leadgenius.app/api/automation/fsd/run/PIPELINE_V1_ID" | jq '{leadsGenerated, leadsEnriched, leadsScored, leadsQualified, leadsSent}'
```

Record these as your V1 baseline: `leadsGenerated`, `leadsEnriched`, `leadsScored`, `leadsQualified`.

**Step 4 — Update ICP targeting criteria based on results**

Refine the ICP by narrowing or adjusting criteria that produced low-quality leads. For example, if leads from small companies scored poorly, remove smaller company sizes.

**API:**

```bash
curl -s -X PUT -H "X-[REDACTED_SECRET]" \
  -H "Content-Type: application/json" \
  -d '{
    "companySizes": ["201-500", "501-1000"],
    "seniority": ["VP", "C-Suite"],
    "industries": ["Software", "SaaS"],
    "apifyInput": "{\"searchUrl\":\"https://linkedin.com/sales/search/people?query=refined-search\"}"
  }' \
  https://api.leadgenius.app/api/automation/tables/ICP/ICP_ID | jq
```

**CLI:**

```bash
python lgp.py tables update ICP ICP_ID --data '{
  "companySizes": ["201-500", "501-1000"],
  "seniority": ["VP", "C-Suite"],
  "apifyInput": "{\"searchUrl\":\"https://linkedin.com/sales/search/people?query=refined-search\"}"
}'
```

**Step 5 — Run a refined generation with the updated ICP**

Trigger a new pipeline run with the same ICP (now updated).

**API:**

```bash
curl -s -X POST -H "X-[REDACTED_SECRET]" \
  -H "Content-Type: application/json" \
  -d '{
    "client_id": "YOUR_CLIENT_ID",
    "icpId": "ICP_ID",
    "targetLeadCount": 100,
    "enrichAfterGeneration": true,
    "scoreAfterEnrichment": true
  }' \
  https://api.leadgenius.app/api/automation/fsd/run | jq
```

**CLI:**

```bash
python lgp.py fsd run --client YOUR_CLIENT_ID --icp ICP_ID --target 100
```

Save the `pipelineId` as `PIPELINE_V2_ID`. Wait for completion.

**Step 6 — Compare before/after metrics**

Compare V1 and V2 pipeline results side by side.

```bash
# V1 metrics
curl -s -H "X-[REDACTED_SECRET]" \
  "https://api.leadgenius.app/api/automation/fsd/run/PIPELINE_V1_ID" | jq '{pipeline: "V1", leadsGenerated, leadsScored, leadsQualified}'

# V2 metrics
curl -s -H "X-[REDACTED_SECRET]" \
  "https://api.leadgenius.app/api/automation/fsd/run/PIPELINE_V2_ID" | jq '{pipeline: "V2", leadsGenerated, leadsScored, leadsQualified}'
```

Key metrics to compare:
- `leadsQualified / leadsScored` ratio — higher means better targeting
- Average `aiLeadScore` across generated leads
- Number of leads matching the desired seniority and industry

If V2 shows improvement, keep the refined ICP. If not, iterate again from Step 4 with different adjustments.

### Common Failure Points and Recovery

| Failure | Cause | Recovery |
|---------|-------|----------|
| V2 scores worse than V1 | Criteria narrowed too aggressively, or Apify search URL doesn't match new criteria | Revert ICP changes and try smaller adjustments; ensure `apifyInput` search URL aligns with targeting criteria |
| Zero leads in V2 | Updated `apifyInput` search URL returns no results | Verify the search URL manually; broaden criteria slightly |
| Scores unchanged between V1 and V2 | Criteria changes didn't affect the lead pool meaningfully | Make more significant changes — try different `industries`, `geographies`, or `jobTitles` |
| Can't compare leads across runs | Both runs generated leads into the same client | Use `createdAt` filtering or create a separate client per run for clean comparison |


---

## 10. Full Automation Setup Workflow

Configure all prerequisites (UrlSettings, AgentSettings, SdrAiSettings, EmailPlatformSettings), create an ICP, create an FSD campaign with all automation flags enabled, and verify the pipeline runs end-to-end without manual intervention.

### When to Use

- Setting up a fully automated demand generation pipeline from scratch
- Onboarding a new client with complete automation capabilities
- Configuring a "set and forget" campaign that generates, enriches, scores, qualifies, and delivers leads automatically

### Steps

**Step 1 — Create UrlSettings for enrichment services**

Configure the URLs and API keys for enrichment services that the pipeline will use during the enrichment stage.

**API:**

```bash
curl -s -X POST -H "X-[REDACTED_SECRET]" \
  -H "Content-Type: application/json" \
  -d '{
    "client_id": "YOUR_CLIENT_ID",
    "companyUrl": "https://enrichment-service.example.com/company",
    "companyUrl_Apikey": "enrich-api-key-1",
    "emailFinder": "https://enrichment-service.example.com/email",
    "emailFinder_Apikey": "enrich-api-key-2",
    "enrichment1": "https://enrichment-service.example.com/data",
    "enrichment1_Apikey": "enrich-api-key-3"
  }' \
  https://api.leadgenius.app/api/automation/tables/UrlSettings | jq
```

**CLI:**

```bash
python lgp.py tables create UrlSettings --data '{
  "client_id": "YOUR_CLIENT_ID",
  "companyUrl": "https://enrichment-service.example.com/company",
  "companyUrl_Apikey": "enrich-api-key-1",
  "emailFinder": "https://enrichment-service.example.com/email",
  "emailFinder_Apikey": "enrich-api-key-2"
}'
```

**Step 2 — Create AgentSettings for copyright (AI content generation)**

Configure the EpsimoAI agent IDs and project ID used during the copyright stage.

**API:**

```bash
curl -s -X POST -H "X-[REDACTED_SECRET]" \
  -H "Content-Type: application/json" \
  -d '{
    "client_id": "YOUR_CLIENT_ID",
    "projectId": "epsimo-project-id",
    "enrichment1AgentId": "agent-id-for-copyright-1",
    "enrichment2AgentId": "agent-id-for-copyright-2"
  }' \
  https://api.leadgenius.app/api/automation/tables/AgentSettings | jq
```

**CLI:**

```bash
python lgp.py tables create AgentSettings --data '{
  "client_id": "YOUR_CLIENT_ID",
  "projectId": "epsimo-project-id",
  "enrichment1AgentId": "agent-id-for-copyright-1"
}'
```

**Step 3 — Create SdrAiSettings for scoring**

Configure the EpsimoAI agent IDs and project ID used during the scoring stage.

**API:**

```bash
curl -s -X POST -H "X-[REDACTED_SECRET]" \
  -H "Content-Type: application/json" \
  -d '{
    "client_id": "YOUR_CLIENT_ID",
    "projectId": "epsimo-sdr-project-id",
    "aiLeadScoreAgentId": "agent-id-for-lead-score",
    "aiQualificationAgentId": "agent-id-for-qualification"
  }' \
  https://api.leadgenius.app/api/automation/tables/SdrAiSettings | jq
```

**CLI:**

```bash
python lgp.py tables create SdrAiSettings --data '{
  "client_id": "YOUR_CLIENT_ID",
  "projectId": "epsimo-sdr-project-id",
  "aiLeadScoreAgentId": "agent-id-for-lead-score",
  "aiQualificationAgentId": "agent-id-for-qualification"
}'
```

**Step 4 — Configure EmailPlatformSettings for delivery**

Set up the email platform integration so qualified leads can be sent automatically.

**API:**

```bash
curl -s -X POST -H "X-[REDACTED_SECRET]" \
  -H "Content-Type: application/json" \
  -d '{
    "client_id": "YOUR_CLIENT_ID",
    "platform": "woodpecker",
    "apiKey": "woodpecker-api-key",
    "baseUrl": "https://api.woodpecker.co"
  }' \
  https://api.leadgenius.app/api/automation/tables/EmailPlatformSettings | jq
```

**CLI:**

```bash
python lgp.py tables create EmailPlatformSettings --data '{
  "client_id": "YOUR_CLIENT_ID",
  "platform": "woodpecker",
  "apiKey": "woodpecker-api-key",
  "baseUrl": "https://api.woodpecker.co"
}'
```

**Step 5 — Create an ICP with Apify configuration**

Define the target profile and lead generation source.

**API:**

```bash
curl -s -X POST -H "X-[REDACTED_SECRET]" \
  -H "Content-Type: application/json" \
  -d '{
    "client_id": "YOUR_CLIENT_ID",
    "name": "Full Automation ICP",
    "industries": ["Software", "SaaS"],
    "companySizes": ["201-500", "501-1000"],
    "geographies": ["United States"],
    "jobTitles": ["VP Sales", "CRO", "Head of Revenue"],
    "seniority": ["VP", "C-Suite"],
    "apifyActorId": "apify/linkedin-sales-navigator",
    "apifyInput": "{\"searchUrl\":\"https://linkedin.com/sales/search/people?query=...\"}",
    "maxLeads": 200
  }' \
  https://api.leadgenius.app/api/automation/tables/ICP | jq
```

Save the returned `id` as `ICP_ID`.

**Step 6 — Create an FSD campaign with all automation flags enabled**

Create a campaign that will automatically enrich, score, qualify, and deliver leads.

**API:**

```bash
curl -s -X POST -H "X-[REDACTED_SECRET]" \
  -H "Content-Type: application/json" \
  -d '{
    "client_id": "YOUR_CLIENT_ID",
    "name": "Full Automation Campaign",
    "icpId": "ICP_ID",
    "frequency": "weekly",
    "targetLeadCount": 200,
    "enrichAfterGeneration": true,
    "scoreAfterEnrichment": true,
    "sendToEmailPlatform": "woodpecker",
    "qualificationThreshold": 60,
    "emailCampaignId": "woodpecker-campaign-id"
  }' \
  https://api.leadgenius.app/api/automation/fsd/campaigns | jq
```

Save the returned `id` as `CAMPAIGN_ID`.

**Step 7 — Run the pipeline and verify end-to-end automation**

Trigger the pipeline. With all flags enabled, it will progress through every stage automatically.

**API:**

```bash
curl -s -X POST -H "X-[REDACTED_SECRET]" \
  -H "Content-Type: application/json" \
  -d '{
    "client_id": "YOUR_CLIENT_ID",
    "icpId": "ICP_ID",
    "targetLeadCount": 200,
    "enrichAfterGeneration": true,
    "scoreAfterEnrichment": true,
    "sendToEmailPlatform": "woodpecker",
    "qualificationThreshold": 60,
    "campaignId": "CAMPAIGN_ID"
  }' \
  https://api.leadgenius.app/api/automation/fsd/run | jq
```

**CLI:**

```bash
python lgp.py fsd run --client YOUR_CLIENT_ID --icp ICP_ID --target 200
```

Monitor until `stage` reaches `completed`:

```bash
python lgp.py fsd status PIPELINE_ID
```

Verify the full pipeline completed all stages:

```bash
curl -s -H "X-[REDACTED_SECRET]" \
  "https://api.leadgenius.app/api/automation/fsd/run/PIPELINE_ID" | jq '{stage, leadsGenerated, leadsEnriched, leadsScored, leadsQualified, leadsSent}'
```

Confirm `leadsSent > 0` to verify leads were delivered to the email platform.

### Common Failure Points and Recovery

| Failure | Cause | Recovery |
|---------|-------|----------|
| Pipeline fails at `enriching` | UrlSettings missing or misconfigured | Verify UrlSettings: `tables list UrlSettings`; ensure service URLs and API keys are correct |
| Pipeline fails at `scoring` | SdrAiSettings missing or agent IDs invalid | Verify SdrAiSettings: `tables get SdrAiSettings SETTINGS_ID`; check agent IDs with EpsimoAI |
| Pipeline completes but `leadsSent` is 0 | `qualificationThreshold` too high — no leads met the score cutoff | Lower the threshold: `fsd update-campaign CAMPAIGN_ID --data '{"qualificationThreshold":40}'` |
| Pipeline completes but `leadsQualified` is 0 | All leads scored below threshold | Review lead scores; consider lowering threshold or improving ICP targeting |
| Email platform delivery fails | EmailPlatformSettings API key invalid or platform unreachable | Verify EmailPlatformSettings; test the platform API key independently |
| `ICP_NO_APIFY` on run | ICP missing `apifyActorId` | Update ICP with Apify config before running |


---

## 11. Campaign Monitoring Dashboard Workflow

List active campaigns, check pipeline run status, review per-stage metrics, identify failed stages, and rerun failed pipelines.

### When to Use

- Daily or weekly review of active FSD campaign performance
- Identifying and recovering from pipeline failures
- Tracking cumulative campaign metrics over time
- Operational monitoring of automated demand generation

### Steps

**Step 1 — List all active campaigns**

Retrieve all FSD campaigns to see their status and cumulative metrics.

**API:**

```bash
curl -s -H "X-[REDACTED_SECRET]" \
  "https://api.leadgenius.app/api/automation/fsd/campaigns?limit=50" | jq '.data.items[] | {id, name, isActive, frequency, totalRuns, totalLeadsGenerated, totalLeadsScored, totalLeadsSent, lastRunAt, nextRunAt}'
```

**CLI:**

```bash
python lgp.py fsd campaigns
```

Review the output for:
- `isActive`: Should be `true` for campaigns you expect to be running.
- `totalRuns`: Number of pipeline runs completed.
- `lastRunAt` / `nextRunAt`: Timing of recent and upcoming runs.
- Cumulative metrics: `totalLeadsGenerated`, `totalLeadsScored`, `totalLeadsSent`.

**Step 2 — Check the latest pipeline run status for a campaign**

Get the details of a specific campaign to find its most recent pipeline run.

**API:**

```bash
curl -s -H "X-[REDACTED_SECRET]" \
  "https://api.leadgenius.app/api/automation/fsd/campaigns/CAMPAIGN_ID" | jq
```

**CLI:**

```bash
python lgp.py fsd campaign CAMPAIGN_ID
```

Note the `lastRunAt` timestamp and any associated pipeline run ID.

**Step 3 — Review per-stage metrics of a pipeline run**

Inspect the pipeline run to see how many leads progressed through each stage.

**API:**

```bash
curl -s -H "X-[REDACTED_SECRET]" \
  "https://api.leadgenius.app/api/automation/fsd/run/PIPELINE_ID" | jq '{stage, leadsGenerated, leadsEnriched, leadsScored, leadsQualified, leadsSent, errorMessage, stageErrors, startedAt, finishedAt}'
```

**CLI:**

```bash
python lgp.py fsd status PIPELINE_ID
```

Key indicators:
- `stage: "completed"` — pipeline finished successfully.
- `stage: "failed"` — pipeline stopped at a stage; check `errorMessage` and `stageErrors`.
- Large drop between stages (e.g., 100 generated but only 10 enriched) — indicates issues at that stage.

**Step 4 — Identify and diagnose failed stages**

If `stage` is `failed`, examine the error details.

```bash
curl -s -H "X-[REDACTED_SECRET]" \
  "https://api.leadgenius.app/api/automation/fsd/run/PIPELINE_ID" | jq '{stage, errorMessage, stageErrors}'
```

Common failure patterns:

| Failed Stage | Likely Cause | Diagnostic Step |
|-------------|-------------|-----------------|
| `generating` | Apify actor error or ICP misconfiguration | Check ICP's `apifyActorId` and `apifyInput` |
| `enriching` | UrlSettings missing or service down | Verify: `tables list UrlSettings` |
| `scoring` | SdrAiSettings missing or agent error | Verify: `tables list SdrAiSettings` |
| `qualifying` | No leads met the `qualificationThreshold` | Review lead scores; consider lowering threshold |
| `sending` | EmailPlatformSettings invalid or platform error | Verify: `tables list EmailPlatformSettings` |

**Step 5 — Rerun a failed pipeline**

After fixing the underlying issue (e.g., updating UrlSettings), trigger a new pipeline run for the same campaign and ICP.

**API:**

```bash
curl -s -X POST -H "X-[REDACTED_SECRET]" \
  -H "Content-Type: application/json" \
  -d '{
    "client_id": "YOUR_CLIENT_ID",
    "icpId": "ICP_ID",
    "targetLeadCount": 200,
    "enrichAfterGeneration": true,
    "scoreAfterEnrichment": true,
    "campaignId": "CAMPAIGN_ID"
  }' \
  https://api.leadgenius.app/api/automation/fsd/run | jq
```

**CLI:**

```bash
python lgp.py fsd run --client YOUR_CLIENT_ID --icp ICP_ID --target 200
```

Monitor the new run to confirm it progresses past the previously failed stage.

```bash
python lgp.py fsd status NEW_PIPELINE_ID
```

**Step 6 — Deactivate a problematic campaign (optional)**

If a campaign is consistently failing and needs investigation, deactivate it to prevent further scheduled runs.

**API:**

```bash
curl -s -X DELETE -H "X-[REDACTED_SECRET]" \
  "https://api.leadgenius.app/api/automation/fsd/campaigns/CAMPAIGN_ID" | jq
```

**CLI:**

```bash
python lgp.py fsd deactivate-campaign CAMPAIGN_ID
```

This sets `isActive: false` (soft delete). Reactivate later by updating the campaign:

```bash
curl -s -X PUT -H "X-[REDACTED_SECRET]" \
  -H "Content-Type: application/json" \
  -d '{"isActive": true}' \
  https://api.leadgenius.app/api/automation/fsd/campaigns/CAMPAIGN_ID | jq
```

### Common Failure Points and Recovery

| Failure | Cause | Recovery |
|---------|-------|----------|
| Campaign shows `isActive: false` unexpectedly | Campaign was deactivated manually or by a failed run | Reactivate: `PUT /fsd/campaigns/ID` with `{"isActive": true}` |
| `nextRunAt` is in the past | Scheduled run was missed (system downtime or campaign was inactive) | Trigger a manual run with `fsd run`; the next scheduled run will be set automatically |
| Cumulative metrics not updating | Pipeline runs are failing before completion | Check the latest pipeline run status for errors |
| Multiple failed runs in a row | Persistent configuration issue | Review all prerequisites (UrlSettings, AgentSettings, SdrAiSettings, EmailPlatformSettings); fix the root cause before rerunning |
| Pipeline stuck (not `completed` or `failed`) | Long-running Apify actor or enrichment service timeout | Wait longer; if stuck for hours, the run may need manual investigation |


---

## 12. Qualification and Delivery Workflow

Filter scored leads by `qualificationThreshold`, route qualified leads to an email platform via `sendToEmailPlatform`, and verify delivery through webhook events.

### When to Use

- After scoring leads and wanting to send only high-quality leads to outreach
- Setting up the qualification-to-delivery leg of an FSD pipeline
- Verifying that qualified leads actually reached the email platform
- Debugging delivery issues when leads are scored but not appearing in the outreach tool

### Steps

**Step 1 — Review scored leads and their qualification status**

After a pipeline run completes scoring, review the leads to understand the score distribution.

**API:**

```bash
curl -s -H "X-[REDACTED_SECRET]" \
  "https://api.leadgenius.app/api/automation/leads?client_id=YOUR_CLIENT_ID&limit=50" | jq '.data.items[] | {id, fullName, companyName, aiLeadScore, aiQualification}'
```

**CLI:**

```bash
python lgp.py leads list --client YOUR_CLIENT_ID --limit 50
```

Note the range of `aiLeadScore` values. Leads with a score at or above the `qualificationThreshold` will be qualified for delivery.

**Step 2 — Understand the qualification threshold**

The `qualificationThreshold` (0–100) set on the FSD campaign or pipeline run determines the cutoff. Only leads with `aiLeadScore >= qualificationThreshold` pass to the `sending` stage.

Check the campaign's threshold:

```bash
curl -s -H "X-[REDACTED_SECRET]" \
  "https://api.leadgenius.app/api/automation/fsd/campaigns/CAMPAIGN_ID" | jq '{qualificationThreshold, sendToEmailPlatform, emailCampaignId}'
```

If the threshold is too high (few leads qualify) or too low (too many unqualified leads sent), update it:

**API:**

```bash
curl -s -X PUT -H "X-[REDACTED_SECRET]" \
  -H "Content-Type: application/json" \
  -d '{"qualificationThreshold": 55}' \
  https://api.leadgenius.app/api/automation/fsd/campaigns/CAMPAIGN_ID | jq
```

**CLI:**

```bash
python lgp.py fsd update-campaign CAMPAIGN_ID --data '{"qualificationThreshold": 55}'
```

**Step 3 — Verify email platform routing configuration**

Confirm that `sendToEmailPlatform` and `emailCampaignId` are set on the campaign so qualified leads are routed correctly.

```bash
curl -s -H "X-[REDACTED_SECRET]" \
  "https://api.leadgenius.app/api/automation/fsd/campaigns/CAMPAIGN_ID" | jq '{sendToEmailPlatform, emailCampaignId, qualificationThreshold}'
```

If `sendToEmailPlatform` is null, update the campaign:

```bash
curl -s -X PUT -H "X-[REDACTED_SECRET]" \
  -H "Content-Type: application/json" \
  -d '{
    "sendToEmailPlatform": "woodpecker",
    "emailCampaignId": "woodpecker-campaign-id"
  }' \
  https://api.leadgenius.app/api/automation/fsd/campaigns/CAMPAIGN_ID | jq
```

Also verify that EmailPlatformSettings exist for the platform:

```bash
python lgp.py tables list EmailPlatformSettings
```

**Step 4 — Check pipeline run delivery metrics**

After a pipeline run completes, verify how many leads were qualified and sent.

**API:**

```bash
curl -s -H "X-[REDACTED_SECRET]" \
  "https://api.leadgenius.app/api/automation/fsd/run/PIPELINE_ID" | jq '{stage, leadsScored, leadsQualified, leadsSent, qualificationThreshold, sendToEmailPlatform}'
```

**CLI:**

```bash
python lgp.py fsd status PIPELINE_ID
```

Key metrics:
- `leadsScored`: Total leads that received scores.
- `leadsQualified`: Leads with `aiLeadScore >= qualificationThreshold`.
- `leadsSent`: Leads successfully sent to the email platform.
- If `leadsQualified > leadsSent`, some deliveries failed — check `stageErrors`.

**Step 5 — Verify delivery via webhook events**

Once leads are sent to the email platform, the platform sends webhook events back (e.g., email opened, clicked, replied). Check for incoming webhook events to confirm delivery.

**API:**

```bash
curl -s -H "X-[REDACTED_SECRET]" \
  "https://api.leadgenius.app/api/automation/webhook-events?limit=20" | jq '.data.items[] | {id, eventType, matchStatus, matchedLeadId, createdAt}'
```

**CLI:**

```bash
python lgp.py webhooks list --limit 20
```

Look for events with `eventType` like `email_sent`, `email_opened`, or `email_clicked` that correspond to the leads you sent.

**Step 6 — Reprocess unmatched webhook events (if needed)**

If webhook events arrived but didn't match to leads (e.g., timing issue), reprocess them.

**API:**

```bash
curl -s -X POST -H "X-[REDACTED_SECRET]" \
  https://api.leadgenius.app/api/automation/webhook-events/EVENT_ID/reprocess | jq
```

**CLI:**

```bash
python lgp.py webhooks reprocess EVENT_ID
```

See [Workflow 3: Webhook Reprocessing](#3-webhook-reprocessing-workflow) for the full reprocessing procedure.

**Step 7 — Send qualified leads manually (alternative)**

If the pipeline didn't have `sendToEmailPlatform` configured, you can send qualified leads manually using the email platforms API.

**API:**

```bash
curl -s -X POST -H "X-[REDACTED_SECRET]" \
  -H "Content-Type: application/json" \
  -d '{
    "platform": "woodpecker",
    "campaignId": "woodpecker-campaign-id",
    "leadIds": ["lead-1", "lead-2", "lead-3"]
  }' \
  https://api.leadgenius.app/api/automation/email-platforms/send | jq
```

### Common Failure Points and Recovery

| Failure | Cause | Recovery |
|---------|-------|----------|
| `leadsQualified` is 0 | All leads scored below `qualificationThreshold` | Lower the threshold on the campaign; or improve ICP targeting to generate higher-quality leads |
| `leadsSent` is 0 but `leadsQualified` > 0 | `sendToEmailPlatform` not configured, or EmailPlatformSettings missing/invalid | Set `sendToEmailPlatform` on the campaign; verify EmailPlatformSettings exist with valid API key |
| No webhook events after sending | Email platform not configured to send webhooks, or webhook URL not set | Configure webhook URL in the email platform's settings to point to your LeadGenius webhook endpoint |
| Webhook events unmatched | Leads were sent but webhook contact data doesn't match lead records | Reprocess unmatched events after verifying lead email addresses match the platform's contact data |
| Threshold too aggressive | Very few leads qualify, reducing campaign volume | Analyze score distribution; set threshold to capture the top 30-50% of scored leads |
| Duplicate sends | Pipeline rerun sends leads that were already delivered | Check if leads were already sent in a previous run before triggering a new pipeline |


path: references/cli_reference.md
content: # LeadGenius Pro — CLI Reference (`lgp`)

The `lgp` CLI is a thin HTTP client wrapping the Automation API. All business logic stays server-side; the CLI is a convenient wrapper for constructing and sending requests.

---

## Installation & Setup

The CLI is a Python script. It requires Python 3.8+ and the `requests` library:

```bash
pip install requests
```

Run the CLI with:

```bash
python lgp.py <command> [options]
```

The script is located at `.agent/skills/leadgenius-api/scripts/lgp.py` in the workspace, or can be copied to any directory.

### Environment Variables

| Variable | Description | Default |
|----------|-------------|---------|
| `LGP_API_KEY` | API key with `lgp_` prefix | — (required) |
| `LGP_ADMIN_KEY` | Admin key to bypass rate limits | — (optional) |
| `LGP_URL` | Base URL of the LeadGenius API | `http://localhost:3000` |

**Recommended setup:**

```bash
export LGP_API_KEY="lgp_your_key_here"
export LGP_ADMIN_KEY="your_admin_key_here"
export LGP_URL="https://api.leadgenius.app"
```

---

## Global Options

These options apply to every command and override environment variables when provided.

| Option | Type | Required | Default | Description |
|--------|------|----------|---------|-------------|
| `--api-key <key>` | string | No | `LGP_API_KEY` env var | API key for authentication (overrides environment variable) |
| `--admin-key <key>` | string | No | `LGP_ADMIN_KEY` env var | Admin key to bypass rate limits (overrides environment variable) |
| `--url <url>` | string | No | `LGP_URL` env var or `http://localhost:3000` | Base URL of the LeadGenius API |
| `--format <fmt>` | string | No | `json` | Output format: `json` or `table` |

When `--admin-key` (or `LGP_ADMIN_KEY`) is set, the CLI sends an `X-Admin-Key` header alongside `X-API-Key`. This bypasses all rate limits. The API key is still required for authentication — the admin key only removes rate limiting.

---

## Output Formats

| Format | Description |
|--------|-------------|
| `json` | Full JSON response body (default). Useful for piping to `jq` or programmatic consumption. |
| `table` | Tabular output with selected columns. Columns vary by command — see below. |

### Table Columns by List Command

| Command | Table Columns |
|---------|---------------|
| `leads list` | `id`, `firstName`, `lastName`, `email`, `companyName`, `status`, `createdAt` |
| `tasks list` | `jobId`, `type`, `status`, `leadId`, `createdAt` |
| `companies list` | `id`, `companyName`, `totalLeads`, `qualifiedLeads`, `averageLeadScore` |
| `webhooks list` | `id`, `platform`, `eventType`, `matched`, `createdAt` |

---

## auth

Test API key validity and connectivity.

### `auth test`

Test that the configured API key is valid and the server is reachable.

**Syntax:**

```bash
lgp auth test
```

**Flags:** None (uses global options only).

**Example:**

```bash
python lgp.py auth test --api-key lgp_abc123def456
```

**Expected output (json):**

```json
{ "success": true, "message": "Authenticated", "company_id": "comp_abc123" }
```

---

## leads

Manage leads: list, get, import, search, deduplicate, transfer, and track engagement.

### `leads list`

List leads for a specific client with optional pagination and field selection.

**Syntax:**

```bash
lgp leads list --client <id> [options]
```

**Flags:**

| Flag | Type | Required | Default | Description |
|------|------|----------|---------|-------------|
| `--client <id>` | string | Yes | — | Client ID to list leads for |
| `--limit <n>` | integer | No | `50` | Maximum number of records to return |
| `--next-token <token>` | string | No | — | Pagination token from a previous response |
| `--fields <f1,f2,...>` | string | No | all fields | Comma-separated list of fields to include in the response |

**Example:**

```bash
python lgp.py leads list \
  --client cl_9f3a2b7e \
  --limit 20 \
  --fields firstName,lastName,email,companyName,aiLeadScore
```

### `leads get`

Retrieve full details for a single lead by ID.

**Syntax:**

```bash
lgp leads get <id>
```

**Flags:**

| Flag | Type | Required | Default | Description |
|------|------|----------|---------|-------------|
| `<id>` | string (positional) | Yes | — | Lead ID to retrieve |

**Example:**

```bash
python lgp.py leads get lead_5c8d1e2f-a3b4-4567-89ef-0123456789ab
```

### `leads import`

Import one or more leads into a client. Provide data via a JSON file or inline JSON string.

**Syntax:**

```bash
lgp leads import --file <path.json>
lgp leads import --data '<json-string>'
```

**Flags:**

| Flag | Type | Required | Default | Description |
|------|------|----------|---------|-------------|
| `--file <path>` | string | One of `--file` or `--data` | — | Path to a JSON file containing lead(s) to import |
| `--data <json>` | string | One of `--file` or `--data` | — | Inline JSON string with lead data. Must include `client_id`. |

**Example — inline single lead:**

```bash
python lgp.py leads import --data '{
  "client_id": "cl_9f3a2b7e",
  "firstName": "Jane",
  "lastName": "Doe",
  "email": "[REDACTED_SECRET]",
  "companyName": "Acme Corp",
  "linkedinUrl": "https://linkedin.com/in/janedoe",
  "title": "VP of Sales"
}'
```

**Example — file import:**

```bash
python lgp.py leads import --file ./leads-batch.json
```

### `leads search`

Search for leads by email, name, company URL, or LinkedIn URL.

**Syntax:**

```bash
lgp leads search [options]
```

**Flags:**

| Flag | Type | Required | Default | Description |
|------|------|----------|---------|-------------|
| `--email <email>` | string | No | — | Search by email address |
| `--first-name <name>` | string | No | — | Search by first name |
| `--last-name <name>` | string | No | — | Search by last name (use with `--first-name`) |
| `--company-url <url>` | string | No | — | Search by company URL |
| `--linkedin-url <url>` | string | No | — | Search by LinkedIn profile URL |
| `--client <id>` | string | No | — | Narrow search to a specific client |
| `--limit <n>` | integer | No | `50` | Maximum results to return |

**Example:**

```bash
python lgp.py leads search \
  --email [REDACTED_SECRET] \
  --client cl_9f3a2b7e
```

### `leads dedup`

Find duplicate leads within a client based on specified match fields.

**Syntax:**

```bash
lgp leads dedup --client <id> --match <fields>
```

**Flags:**

| Flag | Type | Required | Default | Description |
|------|------|----------|---------|-------------|
| `--client <id>` | string | Yes | — | Client ID to scan for duplicates |
| `--match <fields>` | string | Yes | — | Comma-separated match fields: `email`, `linkedinUrl`, `fullName+companyName` |

**Example:**

```bash
python lgp.py leads dedup \
  --client cl_9f3a2b7e \
  --match email,linkedinUrl
```

### `leads dedup-resolve`

Merge duplicate leads by keeping one lead and merging data from others into it.

**Syntax:**

```bash
lgp leads dedup-resolve --keep <leadId> --merge <id1,id2,...>
```

**Flags:**

| Flag | Type | Required | Default | Description |
|------|------|----------|---------|-------------|
| `--keep <leadId>` | string | Yes | — | Lead ID to keep as the primary record |
| `--merge <ids>` | string | Yes | — | Comma-separated lead IDs to merge into the kept lead |

**Example:**

```bash
python lgp.py leads dedup-resolve \
  --keep lead_5c8d1e2f-a3b4-4567-89ef-0123456789ab \
  --merge lead_aaaa1111-bbbb-2222-cccc-333344445555,lead_dddd6666-eeee-7777-ffff-888899990000
```

### `leads transfer`

Transfer leads between clients within the same company.

**Syntax:**

```bash
lgp leads transfer --from <clientId> --to <clientId> [options]
```

**Flags:**

| Flag | Type | Required | Default | Description |
|------|------|----------|---------|-------------|
| `--from <clientId>` | string | Yes | — | Source client ID |
| `--to <clientId>` | string | Yes | — | Target client ID |
| `--leads <id1,id2,...>` | string | No | — | Comma-separated lead IDs to transfer (omit for `--all`) |
| `--all` | boolean | No | `false` | Transfer all leads from source to target |
| `--dry-run` | boolean | No | `false` | Simulate the transfer without making changes |

**Example — dry run:**

```bash
python lgp.py leads transfer \
  --from cl_9f3a2b7e \
  --to cl_4d5e6f7a \
  --all \
  --dry-run
```

### `leads validate-ownership`

Scan for leads with ownership issues (orphaned records, mismatched company assignments).

**Syntax:**

```bash
lgp leads validate-ownership
```

**Flags:** None (uses global options only).

**Example:**

```bash
python lgp.py leads validate-ownership
```

### `leads activity`

Log an engagement activity on a lead.

**Syntax:**

```bash
lgp leads activity <leadId> --type <activityType> [options]
```

**Flags:**

| Flag | Type | Required | Default | Description |
|------|------|----------|---------|-------------|
| `<leadId>` | string (positional) | Yes | — | Lead ID to log activity for |
| `--type <activityType>` | string | Yes | — | Activity type (see list below) |
| `--notes <text>` | string | No | — | Free-text notes about the activity |
| `--metadata <json>` | string | No | — | JSON string with additional metadata |

**Activity types:** `linkedin_connection_sent`, `linkedin_connection_accepted`, `linkedin_message_sent`, `linkedin_message_received`, `linkedin_profile_viewed`, `email_sent`, `email_opened`, `email_clicked`, `email_answered`, `email_bounced`, `call_completed`, `call_no_answer`, `meeting_scheduled`, `meeting_completed`, `form_submitted`, `website_visited`, `content_downloaded`, `demo_requested`, `proposal_sent`, `contract_signed`, `custom`

**Example:**

```bash
python lgp.py leads activity lead_5c8d1e2f-a3b4-4567-89ef-0123456789ab \
  --type email_sent \
  --notes "Initial outreach email" \
  --metadata '{"campaignId":"camp_001","subject":"Quick question about your pipeline"}'
```

### `leads activities`

Retrieve the engagement history for a lead.

**Syntax:**

```bash
lgp leads activities <leadId>
```

**Flags:**

| Flag | Type | Required | Default | Description |
|------|------|----------|---------|-------------|
| `<leadId>` | string (positional) | Yes | — | Lead ID to retrieve engagement history for |

**Example:**

```bash
python lgp.py leads activities lead_5c8d1e2f-a3b4-4567-89ef-0123456789ab
```

---

## tasks

Manage background processing jobs: enrichment, copyright (AI content), and scoring.

### `tasks list`

List background jobs with optional status and type filters.

**Syntax:**

```bash
lgp tasks list [options]
```

**Flags:**

| Flag | Type | Required | Default | Description |
|------|------|----------|---------|-------------|
| `--status <status>` | string | No | — | Filter by status: `running`, `completed`, `failed` |
| `--type <type>` | string | No | — | Filter by type: `enrichment`, `copyright`, `scoring` |
| `--limit <n>` | integer | No | `50` | Maximum number of jobs to return |

**Example:**

```bash
python lgp.py tasks list --status running --type enrichment --limit 10
```

### `tasks status`

Get the current status and details of a specific job.

**Syntax:**

```bash
lgp tasks status <jobId>
```

**Flags:**

| Flag | Type | Required | Default | Description |
|------|------|----------|---------|-------------|
| `<jobId>` | string (positional) | Yes | — | Job ID to check status for |

**Example:**

```bash
python lgp.py tasks status job_7a8b9c0d-1234-5678-abcd-ef0123456789
```

### `tasks enrich`

Trigger enrichment for a lead using configured external data services. Requires UrlSettings to be configured.

**Syntax:**

```bash
lgp tasks enrich --lead <leadId> [options]
```

**Flags:**

| Flag | Type | Required | Default | Description |
|------|------|----------|---------|-------------|
| `--lead <leadId>` | string | Yes | — | Lead ID to enrich |
| `--services <s1,s2,...>` | string | No | all services | Comma-separated enrichment service identifiers to run |

**Enrichment service identifiers:** `companyUrl`, `emailFinder`, `enrichment1` through `enrichment10`

**Example:**

```bash
python lgp.py tasks enrich \
  --lead lead_5c8d1e2f-a3b4-4567-89ef-0123456789ab \
  --services companyUrl,emailFinder,enrichment1
```

### `tasks copyright`

Trigger AI content generation (copyright) for a lead. Requires AgentSettings to be configured.

**Syntax:**

```bash
lgp tasks copyright --lead <leadId> [options]
```

**Flags:**

| Flag | Type | Required | Default | Description |
|------|------|----------|---------|-------------|
| `--lead <leadId>` | string | Yes | — | Lead ID to generate content for |
| `--processes <p1,p2,...>` | string | No | all processes | Comma-separated copyright process identifiers to run |

**Copyright process identifiers:** `enrichment1` through `enrichment10` — each maps to an AI agent configured in AgentSettings.

**Example:**

```bash
python lgp.py tasks copyright \
  --lead lead_5c8d1e2f-a3b4-4567-89ef-0123456789ab \
  --processes enrichment1,enrichment2
```

### `tasks score`

Trigger SDR AI scoring for a lead. Requires SdrAiSettings to be configured.

**Syntax:**

```bash
lgp tasks score --lead <leadId> [options]
```

**Flags:**

| Flag | Type | Required | Default | Description |
|------|------|----------|---------|-------------|
| `--lead <leadId>` | string | Yes | — | Lead ID to score |
| `--fields <f1,f2,...>` | string | No | all fields | Comma-separated scoring field identifiers to compute |

**Scoring field identifiers:** `aiLeadScore`, `aiQualification`, `aiNextAction`, `aiColdEmail`, `aiInterest`, `aiLinkedinConnect`, `aiCompetitorAnalysis`, `aiEngagementLevel`, `aiPurchaseWindow`, `aiDecisionMakerRole`, `aiSentiment`, `aiSocialEngagement`, `aiNurturingStage`, `aiBudgetEstimation`, `aiRiskScore`, `aiProductFitScore`

**Example:**

```bash
python lgp.py tasks score \
  --lead lead_5c8d1e2f-a3b4-4567-89ef-0123456789ab \
  --fields aiLeadScore,aiQualification,aiNextAction
```

---

## companies

Browse territory companies aggregated from lead data.

### `companies list`

List territory companies for a client with optional sorting.

**Syntax:**

```bash
lgp companies list --client <id> [options]
```

**Flags:**

| Flag | Type | Required | Default | Description |
|------|------|----------|---------|-------------|
| `--client <id>` | string | Yes | — | Client ID to list companies for |
| `--sort <field>` | string | No | — | Sort field: `totalLeads`, `qualifiedLeads`, `averageLeadScore`, `lastActivityDate`, `companyName` |
| `--limit <n>` | integer | No | `50` | Maximum number of companies to return |

**Example:**

```bash
python lgp.py companies list \
  --client cl_9f3a2b7e \
  --sort averageLeadScore \
  --limit 25
```

### `companies get`

Get detailed information for a territory company including leads summary.

**Syntax:**

```bash
lgp companies get <id>
```

**Flags:**

| Flag | Type | Required | Default | Description |
|------|------|----------|---------|-------------|
| `<id>` | string (positional) | Yes | — | Territory company ID |

**Example:**

```bash
python lgp.py companies get tc_1a2b3c4d-5678-9abc-def0-123456789abc
```

### `companies leads`

List leads belonging to a specific territory company.

**Syntax:**

```bash
lgp companies leads <id> [options]
```

**Flags:**

| Flag | Type | Required | Default | Description |
|------|------|----------|---------|-------------|
| `<id>` | string (positional) | Yes | — | Territory company ID |
| `--limit <n>` | integer | No | `50` | Maximum number of leads to return |

**Example:**

```bash
python lgp.py companies leads tc_1a2b3c4d-5678-9abc-def0-123456789abc \
  --limit 50
```

### `companies content-analysis`

Re-run content analysis for a territory company.

**Syntax:**

```bash
lgp companies content-analysis <id>
```

**Flags:**

| Flag | Type | Required | Default | Description |
|------|------|----------|---------|-------------|
| `<id>` | string (positional) | Yes | — | Territory company ID to analyze |

**Example:**

```bash
python lgp.py companies content-analysis tc_1a2b3c4d-5678-9abc-def0-123456789abc
```

---

## webhooks

Manage inbound webhook events from email platforms (e.g., Woodpecker).

### `webhooks list`

List webhook events with optional platform and event type filters.

**Syntax:**

```bash
lgp webhooks list [options]
```

**Flags:**

| Flag | Type | Required | Default | Description |
|------|------|----------|---------|-------------|
| `--platform <name>` | string | No | — | Filter by platform name (e.g., `woodpecker`) |
| `--event-type <type>` | string | No | — | Filter by event type (e.g., `email_opened`, `email_clicked`) |
| `--limit <n>` | integer | No | `50` | Maximum number of events to return |

**Example:**

```bash
python lgp.py webhooks list \
  --platform woodpecker \
  --event-type email_opened \
  --limit 30
```

### `webhooks get`

Get full details for a specific webhook event.

**Syntax:**

```bash
lgp webhooks get <id>
```

**Flags:**

| Flag | Type | Required | Default | Description |
|------|------|----------|---------|-------------|
| `<id>` | string (positional) | Yes | — | Webhook event ID |

**Example:**

```bash
python lgp.py webhooks get wh_evt_aabb1122-ccdd-3344-eeff-556677889900
```

### `webhooks reprocess`

Re-run lead matching for a webhook event. Useful for events that initially failed to match a lead.

**Syntax:**

```bash
lgp webhooks reprocess <id>
```

**Flags:**

| Flag | Type | Required | Default | Description |
|------|------|----------|---------|-------------|
| `<id>` | string (positional) | Yes | — | Webhook event ID to reprocess |

**Example:**

```bash
python lgp.py webhooks reprocess wh_evt_aabb1122-ccdd-3344-eeff-556677889900
```


---

## tables

Generic CRUD operations for any DynamoDB table. Commonly used for `ICP`, `Client`, `UrlSettings`, `AgentSettings`, `SdrAiSettings`, and `EmailPlatformSettings` records.

### `tables list`

List records from any table with optional pagination.

**Syntax:**

```bash
lgp tables list <tableName> [options]
```

**Flags:**

| Flag | Type | Required | Default | Description |
|------|------|----------|---------|-------------|
| `<tableName>` | string (positional) | Yes | — | DynamoDB table name (e.g., `ICP`, `Client`, `UrlSettings`) |
| `--limit <n>` | integer | No | `50` | Maximum number of records to return |

**Example — list all ICP records:**

```bash
python lgp.py tables list ICP
```

**Example — list clients:**

```bash
python lgp.py tables list Client --limit 10
```

### `tables create`

Create a new record in any table.

**Syntax:**

```bash
lgp tables create <tableName> --data '<json-string>'
```

**Flags:**

| Flag | Type | Required | Default | Description |
|------|------|----------|---------|-------------|
| `<tableName>` | string (positional) | Yes | — | DynamoDB table name |
| `--data <json>` | string | Yes | — | JSON string containing the record fields |

**Example — create an ICP with targeting criteria and Apify config:**

```bash
python lgp.py tables create ICP --data '{
  "name": "Enterprise SaaS Decision Makers",
  "client_id": "cl_9f3a2b7e",
  "industries": ["SaaS", "Cloud Computing", "Enterprise Software"],
  "companySizes": ["51-200", "201-500", "501-1000"],
  "geographies": ["United States", "United Kingdom", "Germany"],
  "jobTitles": ["VP of Sales", "Head of Revenue", "CRO"],
  "seniority": ["Director", "VP", "C-Suite"],
  "departments": ["Sales", "Revenue Operations"],
  "keywords": ["B2B SaaS", "revenue growth", "pipeline"],
  "technologies": ["Salesforce", "HubSpot", "Outreach"],
  "apifyActorId": "apify/linkedin-sales-navigator",
  "apifyInput": "{\"searchUrl\":\"https://www.linkedin.com/sales/search/people?query=...\"}",
  "maxLeads": 200,
  "isActive": true
}'
```

**Example — create a Client:**

```bash
python lgp.py tables create Client --data '{
  "name": "Q1 Campaign",
  "description": "Leads for Q1 outbound campaign"
}'
```

### `tables get`

Retrieve a single record by ID from any table.

**Syntax:**

```bash
lgp tables get <tableName> <id>
```

**Flags:**

| Flag | Type | Required | Default | Description |
|------|------|----------|---------|-------------|
| `<tableName>` | string (positional) | Yes | — | DynamoDB table name |
| `<id>` | string (positional) | Yes | — | Record ID to retrieve |

**Example — get an ICP record:**

```bash
python lgp.py tables get ICP icp_a1b2c3d4-5678-9abc-def0-123456789abc
```

### `tables update`

Update fields on an existing record. Only the fields provided in `--data` are modified.

**Syntax:**

```bash
lgp tables update <tableName> <id> --data '<json-string>'
```

**Flags:**

| Flag | Type | Required | Default | Description |
|------|------|----------|---------|-------------|
| `<tableName>` | string (positional) | Yes | — | DynamoDB table name |
| `<id>` | string (positional) | Yes | — | Record ID to update |
| `--data <json>` | string | Yes | — | JSON string with fields to update |

**Example — update ICP targeting criteria:**

```bash
python lgp.py tables update ICP icp_a1b2c3d4-5678-9abc-def0-123456789abc --data '{
  "jobTitles": ["VP of Sales", "Head of Revenue", "CRO", "Director of Sales"],
  "companySizes": ["201-500", "501-1000", "1001-5000"]
}'
```

**Example — deactivate an ICP:**

```bash
python lgp.py tables update ICP icp_a1b2c3d4-5678-9abc-def0-123456789abc --data '{
  "isActive": false
}'
```

### `tables delete`

Delete a record from any table.

**Syntax:**

```bash
lgp tables delete <tableName> <id>
```

**Flags:**

| Flag | Type | Required | Default | Description |
|------|------|----------|---------|-------------|
| `<tableName>` | string (positional) | Yes | — | DynamoDB table name |
| `<id>` | string (positional) | Yes | — | Record ID to delete |

**Example — delete an ICP:**

```bash
python lgp.py tables delete ICP icp_a1b2c3d4-5678-9abc-def0-123456789abc
```

---

## email-platforms

Manage email platform integrations for outbound delivery.

### `email-platforms list`

List all configured email platforms for the company.

**Syntax:**

```bash
lgp email-platforms list
```

**Flags:** None (uses global options only).

**Example:**

```bash
python lgp.py email-platforms list
```

### `email-platforms send`

Send qualified leads to an email platform campaign.

**Syntax:**

```bash
lgp email-platforms send --platform <name> --campaign <id> --leads <id1,id2,...>
```

**Flags:**

| Flag | Type | Required | Default | Description |
|------|------|----------|---------|-------------|
| `--platform <name>` | string | Yes | — | Email platform name (e.g., `woodpecker`) |
| `--campaign <id>` | string | Yes | — | Campaign ID on the email platform |
| `--leads <ids>` | string | Yes | — | Comma-separated lead IDs to send |

**Example:**

```bash
python lgp.py email-platforms send \
  --platform woodpecker \
  --campaign camp_12345 \
  --leads lead_5c8d1e2f-a3b4-4567-89ef-0123456789ab,lead_aaaa1111-bbbb-2222-cccc-333344445555
```

---

## users

Manage company users: list, create, update, delete, and provision.

### `users list`

List users in the company with optional group filter.

**Syntax:**

```bash
lgp users list [options]
```

**Flags:**

| Flag | Type | Required | Default | Description |
|------|------|----------|---------|-------------|
| `--group <group>` | string | No | — | Filter by group: `admin`, `manager`, `user`, `viewer` |
| `--limit <n>` | integer | No | `50` | Maximum number of users to return |

**Example:**

```bash
python lgp.py users list --group admin --limit 20
```

### `users get`

Get full details for a specific user.

**Syntax:**

```bash
lgp users get <id>
```

**Flags:**

| Flag | Type | Required | Default | Description |
|------|------|----------|---------|-------------|
| `<id>` | string (positional) | Yes | — | User ID to retrieve |

**Example:**

```bash
python lgp.py users get usr_1a2b3c4d-5678-9abc-def0-123456789abc
```

### `users create`

Create or invite a new user into the company.

**Syntax:**

```bash
lgp users create --email <email> [options]
```

**Flags:**

| Flag | Type | Required | Default | Description |
|------|------|----------|---------|-------------|
| `--email <email>` | string | Yes | — | User's email address |
| `--role <role>` | string | No | `member` | Role: `owner`, `admin`, `member`, `viewer` |
| `--group <group>` | string | No | `user` | Group: `admin`, `manager`, `user`, `viewer` |

**Example:**

```bash
python lgp.py users create \
  --email [REDACTED_SECRET] \
  --role member \
  --group user
```

### `users update`

Update an existing user's role, group, or status.

**Syntax:**

```bash
lgp users update <id> [options]
```

**Flags:**

| Flag | Type | Required | Default | Description |
|------|------|----------|---------|-------------|
| `<id>` | string (positional) | Yes | — | User ID to update |
| `--role <role>` | string | No | — | New role: `owner`, `admin`, `member`, `viewer` |
| `--group <group>` | string | No | — | New group: `admin`, `manager`, `user`, `viewer` |
| `--status <status>` | string | No | — | New status: `pending`, `active`, `inactive`, `disabled` |

**Example:**

```bash
python lgp.py users update usr_1a2b3c4d-5678-9abc-def0-123456789abc \
  --role admin \
  --group admin
```

### `users delete`

Remove a user from the company.

**Syntax:**

```bash
lgp users delete <id>
```

**Flags:**

| Flag | Type | Required | Default | Description |
|------|------|----------|---------|-------------|
| `<id>` | string (positional) | Yes | — | User ID to remove |

**Example:**

```bash
python lgp.py users delete usr_1a2b3c4d-5678-9abc-def0-123456789abc
```

### `users provision`

Full user provisioning: creates a Cognito user, company (or joins existing), CompanyUser record, and API key in a single operation.

**Syntax:**

```bash
lgp users provision --email <email> --password <password> [options]
```

**Flags:**

| Flag | Type | Required | Default | Description |
|------|------|----------|---------|-------------|
| `--email <email>` | string | Yes | — | User's email address |
| `--password <password>` | string | Yes | — | Initial password (must meet Cognito policy) |
| `--name <name>` | string | No | — | User's display name |
| `--company-name <name>` | string | No | — | Create a new company with this name |
| `--company-id <id>` | string | No | — | Join an existing company by ID (alternative to `--company-name`) |
| `--role <role>` | string | No | `member` | Role: `owner`, `admin`, `member`, `viewer` |
| `--group <group>` | string | No | `user` | Group: `admin`, `manager`, `user`, `viewer` |

**Example — provision with new company:**

```bash
python lgp.py users provision \
  --email [REDACTED_SECRET] \
  --password "SecurePass123!" \
  --name "Admin User" \
  --company-name "New Corp" \
  --role owner \
  --group admin
```

**Example — provision into existing company:**

```bash
python lgp.py users provision \
  --email [REDACTED_SECRET] \
  --password "SecurePass456!" \
  --name "Team Member" \
  --company-id comp_abc123 \
  --role member \
  --group user
```

---

## cognito

Manage AWS Cognito user accounts directly (low-level user pool operations).

### `cognito list`

List Cognito users in the user pool.

**Syntax:**

```bash
lgp cognito list [options]
```

**Flags:**

| Flag | Type | Required | Default | Description |
|------|------|----------|---------|-------------|
| `--limit <n>` | integer | No | `50` | Maximum number of users to return |

**Example:**

```bash
python lgp.py cognito list --limit 20
```

### `cognito get`

Get a Cognito user by email address.

**Syntax:**

```bash
lgp cognito get --email <email>
```

**Flags:**

| Flag | Type | Required | Default | Description |
|------|------|----------|---------|-------------|
| `--email <email>` | string | Yes | — | Email address of the Cognito user to retrieve |

**Example:**

```bash
python lgp.py cognito get --email [REDACTED_SECRET]
```

### `cognito create`

Create a new Cognito user in the user pool.

**Syntax:**

```bash
lgp cognito create --email <email> --password <password> [options]
```

**Flags:**

| Flag | Type | Required | Default | Description |
|------|------|----------|---------|-------------|
| `--email <email>` | string | Yes | — | Email address for the new user |
| `--password <password>` | string | Yes | — | Initial password (must meet Cognito password policy) |
| `--name <name>` | string | No | — | User's display name |

**Example:**

```bash
python lgp.py cognito create \
  --email [REDACTED_SECRET] \
  --password "SecurePass789!" \
  --name "New User"
```

### `cognito enable`

Enable a disabled Cognito user account.

**Syntax:**

```bash
lgp cognito enable --email <email>
```

**Flags:**

| Flag | Type | Required | Default | Description |
|------|------|----------|---------|-------------|
| `--email <email>` | string | Yes | — | Email address of the Cognito user to enable |

**Example:**

```bash
python lgp.py cognito enable --email [REDACTED_SECRET]
```

### `cognito disable`

Disable a Cognito user account (prevents login without deleting the account).

**Syntax:**

```bash
lgp cognito disable --email <email>
```

**Flags:**

| Flag | Type | Required | Default | Description |
|------|------|----------|---------|-------------|
| `--email <email>` | string | Yes | — | Email address of the Cognito user to disable |

**Example:**

```bash
python lgp.py cognito disable --email [REDACTED_SECRET]
```

---

## org

Manage companies (organizations): create, rename, delete, and manage user membership.

### `org list`

List companies accessible to the authenticated user.

**Syntax:**

```bash
lgp org list [options]
```

**Flags:**

| Flag | Type | Required | Default | Description |
|------|------|----------|---------|-------------|
| `--limit <n>` | integer | No | `50` | Maximum number of companies to return |

**Example:**

```bash
python lgp.py org list --limit 10
```

### `org get`

Get detailed information for a specific company.

**Syntax:**

```bash
lgp org get <id>
```

**Flags:**

| Flag | Type | Required | Default | Description |
|------|------|----------|---------|-------------|
| `<id>` | string (positional) | Yes | — | Company ID to retrieve |

**Example:**

```bash
python lgp.py org get comp_abc123
```

### `org create`

Create a new company.

**Syntax:**

```bash
lgp org create --name <name>
```

**Flags:**

| Flag | Type | Required | Default | Description |
|------|------|----------|---------|-------------|
| `--name <name>` | string | Yes | — | Company name |

**Example:**

```bash
python lgp.py org create --name "Acme Corp"
```

### `org rename`

Rename an existing company.

**Syntax:**

```bash
lgp org rename <id> --name <newName>
```

**Flags:**

| Flag | Type | Required | Default | Description |
|------|------|----------|---------|-------------|
| `<id>` | string (positional) | Yes | — | Company ID to rename |
| `--name <newName>` | string | Yes | — | New company name |

**Example:**

```bash
python lgp.py org rename comp_abc123 --name "Acme Corporation"
```

### `org delete`

Delete a company. Only the company owner can perform this action.

**Syntax:**

```bash
lgp org delete <id>
```

**Flags:**

| Flag | Type | Required | Default | Description |
|------|------|----------|---------|-------------|
| `<id>` | string (positional) | Yes | — | Company ID to delete |

**Example:**

```bash
python lgp.py org delete comp_abc123
```

### `org users`

List users belonging to a specific company.

**Syntax:**

```bash
lgp org users <companyId> [options]
```

**Flags:**

| Flag | Type | Required | Default | Description |
|------|------|----------|---------|-------------|
| `<companyId>` | string (positional) | Yes | — | Company ID to list users for |
| `--limit <n>` | integer | No | `50` | Maximum number of users to return |

**Example:**

```bash
python lgp.py org users comp_abc123 --limit 20
```

### `org add-user`

Add a user to a company with a specified role and group.

**Syntax:**

```bash
lgp org add-user <companyId> --email <email> [options]
```

**Flags:**

| Flag | Type | Required | Default | Description |
|------|------|----------|---------|-------------|
| `<companyId>` | string (positional) | Yes | — | Company ID to add the user to |
| `--email <email>` | string | Yes | — | Email address of the user to add |
| `--user-id <cognitoSub>` | string | No | — | Cognito user sub (resolved from email if omitted) |
| `--role <role>` | string | No | `member` | Role: `owner`, `admin`, `member`, `viewer` |
| `--group <group>` | string | No | `user` | Group: `admin`, `manager`, `user`, `viewer` |

**Example:**

```bash
python lgp.py org add-user comp_abc123 \
  --email [REDACTED_SECRET] \
  --role member \
  --group user
```

### `org update-user`

Update a user's role, group, or status within a company.

**Syntax:**

```bash
lgp org update-user <userId> [options]
```

**Flags:**

| Flag | Type | Required | Default | Description |
|------|------|----------|---------|-------------|
| `<userId>` | string (positional) | Yes | — | User ID to update |
| `--role <role>` | string | No | — | New role: `owner`, `admin`, `member`, `viewer` |
| `--group <group>` | string | No | — | New group: `admin`, `manager`, `user`, `viewer` |
| `--status <status>` | string | No | — | New status: `active`, `inactive`, `pending` |

**Example:**

```bash
python lgp.py org update-user usr_1a2b3c4d-5678-9abc-def0-123456789abc \
  --role admin \
  --group admin \
  --status active
```

### `org remove-user`

Remove a user from a company.

**Syntax:**

```bash
lgp org remove-user <userId>
```

**Flags:**

| Flag | Type | Required | Default | Description |
|------|------|----------|---------|-------------|
| `<userId>` | string (positional) | Yes | — | User ID to remove from the company |

**Example:**

```bash
python lgp.py org remove-user usr_1a2b3c4d-5678-9abc-def0-123456789abc
```

---

## fsd

Manage FSD (Full-Stack Demand) pipeline campaigns: create, run, monitor, and manage automated lead generation pipelines.

### `fsd campaigns`

List all FSD campaigns.

**Syntax:**

```bash
lgp fsd campaigns
```

**Flags:** None (uses global options only).

**Example:**

```bash
python lgp.py fsd campaigns
```

### `fsd campaign`

Get detailed information for a specific FSD campaign.

**Syntax:**

```bash
lgp fsd campaign <id>
```

**Flags:**

| Flag | Type | Required | Default | Description |
|------|------|----------|---------|-------------|
| `<id>` | string (positional) | Yes | — | Campaign ID to retrieve |

**Example:**

```bash
python lgp.py fsd campaign fsd_camp_a1b2c3d4
```

### `fsd create-campaign`

Create a new FSD campaign linking an ICP or direct Apify configuration.

**Syntax:**

```bash
lgp fsd create-campaign --client <clientId> --name <name> [options]
```

**Flags:**

| Flag | Type | Required | Default | Description |
|------|------|----------|---------|-------------|
| `--client <clientId>` | string | Yes | — | Client ID for lead data isolation |
| `--name <name>` | string | Yes | — | Campaign display name |
| `--icp <icpId>` | string | No | — | ICP record ID — resolves Apify config automatically |
| `--actor <apifyActorId>` | string | No | — | Direct Apify actor ID (alternative to `--icp`) |
| `--input <json>` | string | No | — | Apify input JSON (used with `--actor`) |
| `--frequency <freq>` | string | No | `once` | Run frequency: `once`, `daily`, `weekly`, `monthly` |
| `--target <n>` | integer | No | `100` | Target lead count per run |
| `--enrich <bool>` | boolean | No | `false` | Auto-enrich leads after generation |
| `--score <bool>` | boolean | No | `false` | Auto-score leads after enrichment |
| `--send-to <platform>` | string | No | — | Email platform name for delivery |
| `--threshold <n>` | integer | No | — | Qualification score threshold (0–100) |
| `--email-campaign <id>` | string | No | — | Campaign ID on the email platform |

**Example — create with ICP and full automation:**

```bash
python lgp.py fsd create-campaign \
  --client cl_9f3a2b7e \
  --name "Q1 Enterprise Outreach" \
  --icp icp_a1b2c3d4-5678-9abc-def0-123456789abc \
  --frequency weekly \
  --target 200 \
  --enrich true \
  --score true \
  --send-to woodpecker \
  --threshold 60 \
  --email-campaign camp-wp-001
```

### `fsd update-campaign`

Update settings on an existing FSD campaign.

**Syntax:**

```bash
lgp fsd update-campaign <id> [options]
```

**Flags:**

| Flag | Type | Required | Default | Description |
|------|------|----------|---------|-------------|
| `<id>` | string (positional) | Yes | — | Campaign ID to update |
| `--name <name>` | string | No | — | New campaign name |
| `--target <n>` | integer | No | — | Target lead count per run |
| `--frequency <freq>` | string | No | — | Run frequency: `once`, `daily`, `weekly`, `monthly` |
| `--active <bool>` | boolean | No | — | Set campaign active status |
| `--enrich <bool>` | boolean | No | — | Enable/disable auto-enrichment after generation |
| `--score <bool>` | boolean | No | — | Enable/disable auto-scoring after enrichment |

**Example:**

```bash
python lgp.py fsd update-campaign fsd_camp_a1b2c3d4 \
  --target 300 \
  --frequency weekly \
  --enrich true \
  --score true
```

### `fsd deactivate-campaign`

Soft-delete a campaign by setting `isActive` to false.

**Syntax:**

```bash
lgp fsd deactivate-campaign <id>
```

**Flags:**

| Flag | Type | Required | Default | Description |
|------|------|----------|---------|-------------|
| `<id>` | string (positional) | Yes | — | Campaign ID to deactivate |

**Example:**

```bash
python lgp.py fsd deactivate-campaign fsd_camp_a1b2c3d4
```

### `fsd run`

Start an FSD pipeline run. Provide either `--icp` to use an ICP record's Apify config, or `--actor`/`--input` for direct Apify execution.

**Syntax:**

```bash
lgp fsd run --client <clientId> [options]
```

**Flags:**

| Flag | Type | Required | Default | Description |
|------|------|----------|---------|-------------|
| `--client <clientId>` | string | Yes | — | Client ID for lead data isolation |
| `--icp <icpId>` | string | No | — | ICP record ID — resolves `apifyActorId` and `apifyInput` from the ICP automatically |
| `--actor <apifyActorId>` | string | No | — | Direct Apify actor ID (alternative to `--icp`) |
| `--input <json>` | string | No | — | Apify input JSON (used with `--actor`) |
| `--target <n>` | integer | No | `100` | Target number of leads to generate |
| `--enrich` | boolean | No | `false` | Auto-enrich leads after generation |
| `--score` | boolean | No | `false` | Auto-score leads after enrichment |

**Example — ICP-driven run:**

```bash
python lgp.py fsd run \
  --client cl_9f3a2b7e \
  --icp icp_a1b2c3d4-5678-9abc-def0-123456789abc \
  --target 200 \
  --enrich \
  --score
```

**Example — direct Apify run (without ICP):**

```bash
python lgp.py fsd run \
  --client cl_9f3a2b7e \
  --actor apify/linkedin-sales-navigator \
  --input '{"searchUrl":"https://www.linkedin.com/sales/search/people?query=..."}' \
  --target 150
```

### `fsd status`

Get the current status and per-stage metrics of an FSD pipeline run.

**Syntax:**

```bash
lgp fsd status <pipelineId>
```

**Flags:**

| Flag | Type | Required | Default | Description |
|------|------|----------|---------|-------------|
| `<pipelineId>` | string (positional) | Yes | — | Pipeline run ID to check |

**Example:**

```bash
python lgp.py fsd status pipeline_x1y2z3w4
```

---

## EpsimoAI Commands

Manage EpsimoAI user activation, profile, credits, and thread usage. All commands pass `X-Epsimo-Token` as an extra header alongside `X-API-Key` (the `apiRequest` function was extended to accept optional `extraHeaders`).

### `epsimo activate`

Activate an EpsimoAI user via email/password or Cognito token exchange.

**Syntax:**

```bash
lgp epsimo activate --email <email> --password <password>
lgp epsimo activate --cognito-token <token>
```

**Flags:**

| Flag | Type | Required | Description |
|------|------|----------|-------------|
| `--email` | string | Yes (login mode) | EpsimoAI email |
| `--password` | string | Yes (login mode) | EpsimoAI password |
| `--cognito-token` | string | Yes (exchange mode) | Cognito ID token |

**Behavior:**
- If `--cognito-token` provided → sends `{ cognitoIdToken }` to activate endpoint
- If `--email` + `--password` provided → sends `{ email, password }`
- If neither → exits with error
- On success, prints the EpsimoAI token with a save warning

**Example:**

```bash
python lgp.py epsimo activate --email [REDACTED_SECRET] --password secret
python lgp.py epsimo activate --cognito-token eyJ...
```

### `epsimo info`

Get EpsimoAI user profile and plan information.

**Syntax:**

```bash
lgp epsimo info --token <epsimoToken>
```

**Flags:**

| Flag | Type | Required | Description |
|------|------|----------|-------------|
| `--token` | string | Yes | EpsimoAI JWT token |

**Table columns (--format table):** `userId`, `email`, `projectId`, `threadCounter`, `threadMax`, `plan`

**Example:**

```bash
python lgp.py epsimo info --token eyJ...
python lgp.py epsimo info --token eyJ... --format table
```

### `epsimo credits`

Check EpsimoAI credit balance.

**Syntax:**

```bash
lgp epsimo credits --token <epsimoToken>
```

**Flags:**

| Flag | Type | Required | Description |
|------|------|----------|-------------|
| `--token` | string | Yes | EpsimoAI JWT token |

**Table columns (--format table):** `credits`, `threadCounter`, `threadMax`, `lastUpdated`

**Example:**

```bash
python lgp.py epsimo credits --token eyJ...
```

### `epsimo purchase`

Purchase additional EpsimoAI credits.

**Syntax:**

```bash
lgp epsimo purchase --token <epsimoToken> --amount <n>
```

**Flags:**

| Flag | Type | Required | Description |
|------|------|----------|-------------|
| `--token` | string | Yes | EpsimoAI JWT token |
| `--amount` | integer | Yes | Number of credits to purchase (positive) |

**Behavior:** Parses `--amount` with `parseInt`, validates > 0 before sending. Sends `{ amount }` body with `X-Epsimo-Token` header.

**Table columns (--format table):** `previousBalance`, `purchasedAmount`, `newBalance`, `transactionId`

**Example:**

```bash
python lgp.py epsimo purchase --token eyJ... --amount 10000
```

### `epsimo threads`

Get detailed thread usage information.

**Syntax:**

```bash
lgp epsimo threads --token <epsimoToken>
```

**Flags:**

| Flag | Type | Required | Description |
|------|------|----------|-------------|
| `--token` | string | Yes | EpsimoAI JWT token |

**Table columns (--format table):** `threadCounter`, `threadMax`, `remainingThreads`, `usagePercentage`, `plan`

**Note:** `plan` is derived from `threadMax` only (without `stripeClientId`), so may differ from `epsimo info` for users with a Stripe subscription and low thread max.

**Example:**

```bash
python lgp.py epsimo threads --token eyJ...
python lgp.py epsimo threads --token eyJ... --format table
```


---

## clients

Manage clients (data partitions) for your company.

### `clients list`

List all clients belonging to your company.

**Syntax:**

```bash
lgp clients list
```

**Example:**

```bash
python lgp.py clients list
```

**Expected output:**

```
ID                                       Client ID                       Name
---------------------------------------  ------------------------------  --------------------
rec-uuid-1                               client-acme-q1                  Q1 Campaign
rec-uuid-2                               client-acme-q2                  Q2 Outbound
```

---

### `clients create`

Create a new client partition.

**Syntax:**

```bash
lgp clients create --name <name> [--url <url>]
```

**Flags:**

| Flag | Type | Required | Default | Description |
|------|------|----------|---------|-------------|
| `--name <name>` | string | Yes | — | Display name for the client |
| `--url <url>` | string | No | — | Company website URL |

**Example:**

```bash
python lgp.py clients create --name "Q3 Campaign" --url "https://acme.com"
```

**Expected output:**

```
Client created:
  ID:        rec-uuid-3
  Client ID: client-acme-q3
```

---

## maintenance

Report bugs and request enhancements via the `lgp` CLI. These go through the Automation API (`/api/agent/maintenance/*`) using your API key.

> For standalone Node.js scripts that bypass the API, see the [Maintenance CLI (Standalone Node.js Scripts)](#maintenance-cli-standalone-nodejs-scripts) section in SKILL.md.

### `maintenance bugs list`

List all reported bugs.

**Syntax:**

```bash
lgp maintenance bugs list
```

**Example:**

```bash
python lgp.py maintenance bugs list
```

---

### `maintenance bugs report`

Report a new bug.

**Syntax:**

```bash
lgp maintenance bugs report --desc <description> [--email <email>]
```

**Flags:**

| Flag | Type | Required | Default | Description |
|------|------|----------|---------|-------------|
| `--desc <text>` | string | Yes | — | Description of the bug |
| `--email <email>` | string | No | — | Contact email for follow-up |

**Example:**

```bash
python lgp.py maintenance bugs report --desc "Login fails on mobile Safari" --email [REDACTED_SECRET]
```

**Expected output:**

```
Bug reported: ID 0917ea6f-a1ae-437f-b085-d776309e0a69
```

---

### `maintenance enhancements list`

List all enhancement requests.

**Syntax:**

```bash
lgp maintenance enhancements list
```

**Example:**

```bash
python lgp.py maintenance enhancements list
```

---

### `maintenance enhancements request`

Submit a new enhancement request.

**Syntax:**

```bash
lgp maintenance enhancements request --desc <description> [--email <email>]
```

**Flags:**

| Flag | Type | Required | Default | Description |
|------|------|----------|---------|-------------|
| `--desc <text>` | string | Yes | — | Description of the enhancement |
| `--email <email>` | string | No | — | Contact email for follow-up |

**Example:**

```bash
python lgp.py maintenance enhancements request --desc "Add bulk CSV export for enriched leads"
```

**Expected output:**

```
Enhancement requested: ID fb3af1d4-21b3-4611-...
```

---

## pipeline

Show pipeline analytics for a date range.

### `pipeline`

**Syntax:**

```bash
lgp pipeline [--start <YYYY-MM-DD>] [--end <YYYY-MM-DD>]
```

**Flags:**

| Flag | Type | Required | Default | Description |
|------|------|----------|---------|-------------|
| `--start <date>` | string | No | 30 days ago | Start date (YYYY-MM-DD) |
| `--end <date>` | string | No | today | End date (YYYY-MM-DD) |

**Example:**

```bash
python lgp.py pipeline --start 2026-01-01 --end 2026-03-31
```

---

## campaigns

Manage campaigns.

### `campaigns list`

List all campaigns.

**Syntax:**

```bash
lgp campaigns list
```

**Example:**

```bash
python lgp.py campaigns list
```

---

### `campaigns create`

Create a new campaign.

**Syntax:**

```bash
lgp campaigns create --name <name>
```

**Flags:**

| Flag | Type | Required | Default | Description |
|------|------|----------|---------|-------------|
| `--name <name>` | string | Yes | — | Campaign name |

**Example:**

```bash
python lgp.py campaigns create --name "Q2 ABM Campaign"
```

---

## generate

Trigger, monitor, and schedule multi-provider lead generation. Supports ICP-based runs (resolve provider config from an ICP record) and direct provider runs (specify provider and config inline).

### `generate from-icp`

Trigger lead generation from an ICP profile. The ICP's `providerType` determines which provider is used (defaults to `apify`).

**Syntax:**

```bash
lgp generate from-icp --icp <icpId> --client <clientId> [options]
```

**Flags:**

| Flag | Type | Required | Default | Description |
|------|------|----------|---------|-------------|
| `--icp <icpId>` | string | Yes | — | ICP record ID |
| `--client <clientId>` | string | Yes | — | Client ID for lead data isolation |
| `--max-leads <n>` | integer | No | ICP default | Override max leads per run |
| `--save-to-source` | boolean | No | `true` | Save leads to SourceLeads table |
| `--save-to-enrich` | boolean | No | `false` | Save leads to EnrichLeads table |
| `--provider <name>` | string | No | ICP default | Override provider: `apify`, `vayne`, `generic` |

**Example:**

```bash
python lgp.py generate from-icp \
  --icp icp_a1b2c3d4-5678-9abc-def0-123456789abc \
  --client cl_9f3a2b7e \
  --max-leads 200 \
  --save-to-enrich
```

**Expected output (json):**

```json
{
  "success": true,
  "runId": "trigger-run-id",
  "searchHistoryId": "sh-uuid",
  "status": "initiated"
}
```

### `generate direct`

Trigger lead generation by specifying a provider and configuration directly (without an ICP).

**Syntax:**

```bash
lgp generate direct --provider <name> --config <json> --client <clientId> [options]
```

**Flags:**

| Flag | Type | Required | Default | Description |
|------|------|----------|---------|-------------|
| `--provider <name>` | string | Yes | — | Provider name: `apify`, `vayne`, `generic` |
| `--config <json>` | string | Yes | — | JSON string with provider-specific configuration |
| `--client <clientId>` | string | Yes | — | Client ID for lead data isolation |
| `--sales-nav-url <url>` | string | No | — | LinkedIn Sales Navigator URL (shorthand for Vayne; injected into `providerConfig.salesNavigatorUrl`) |

**Example — Vayne with Sales Navigator URL:**

```bash
python lgp.py generate direct \
  --provider vayne \
  --config '{"maxLeads":100}' \
  --client cl_9f3a2b7e \
  --sales-nav-url "https://www.linkedin.com/sales/search/people?query=..."
```

**Example — Generic HTTP provider:**

```bash
python lgp.py generate direct \
  --provider generic \
  --config '{"endpointUrl":"https://api.example.com/scrape","method":"POST","bodyTemplate":{"query":"SaaS","limit":50},"responseMapping":{"firstName":"first_name","email":"contact_email"}}' \
  --client cl_9f3a2b7e
```

### `generate status`

Check the status of a lead generation run.

**Syntax:**

```bash
lgp generate status <runId>
```

**Flags:**

| Flag | Type | Required | Default | Description |
|------|------|----------|---------|-------------|
| `<runId>` | string (positional) | Yes | — | Run ID from the trigger response |

**Example:**

```bash
python lgp.py generate status run_abc123def456
```

**Expected output (json):**

```json
{
  "success": true,
  "runId": "run_abc123def456",
  "status": "RUNNING",
  "progress": 45,
  "totalLeadsFound": 67,
  "totalLeadsSaved": 45,
  "totalLeadsFailed": 2,
  "providerType": "apify",
  "searchHistoryId": "sh-uuid"
}
```

**Table columns (--format table):** `runId`, `status`, `progress`, `totalLeadsFound`, `totalLeadsSaved`, `providerType`

### `generate history`

List past lead generation runs with optional filters.

**Syntax:**

```bash
lgp generate history [options]
```

**Flags:**

| Flag | Type | Required | Default | Description |
|------|------|----------|---------|-------------|
| `--client <clientId>` | string | No | — | Filter by client ID |
| `--icp <icpId>` | string | No | — | Filter by ICP ID |
| `--status <status>` | string | No | — | Filter by status: `initiated`, `running`, `completed`, `failed` |
| `--limit <n>` | integer | No | 20 | Maximum records to return |

**Example:**

```bash
python lgp.py generate history --client cl_9f3a2b7e --status completed --limit 10
```

**Table columns (--format table):** `id`, `searchName`, `status`, `providerType`, `totalLeadsFound`, `totalLeadsSaved`, `createdAt`

### `generate schedule create`

Create an FSD schedule for recurring lead generation.

**Syntax:**

```bash
lgp generate schedule create --icp <icpId> --client <clientId> --frequency <freq> [options]
```

**Flags:**

| Flag | Type | Required | Default | Description |
|------|------|----------|---------|-------------|
| `--icp <icpId>` | string | Yes | — | ICP record ID |
| `--client <clientId>` | string | Yes | — | Client ID for lead isolation |
| `--frequency <freq>` | string | Yes | — | Preset (`daily`, `weekly`, `biweekly`, `monthly`) or cron expression |
| `--max-leads <n>` | integer | No | 100 | Max leads per scheduled run |

**Example:**

```bash
python lgp.py generate schedule create \
  --icp icp_a1b2c3d4-5678-9abc-def0-123456789abc \
  --client cl_9f3a2b7e \
  --frequency weekly \
  --max-leads 150
```

**Expected output (json):**

```json
{
  "success": true,
  "scheduleId": "sched-uuid",
  "nextRunAt": "2025-01-22T08:00:00Z",
  "frequency": "weekly"
}
```

### `generate schedule list`

List all FSD schedules for the company.

**Syntax:**

```bash
lgp generate schedule list
```

**Flags:** None (uses global options only).

**Example:**

```bash
python lgp.py generate schedule list --format table
```

**Table columns (--format table):** `id`, `icpName`, `clientName`, `frequencyPreset`, `enabled`, `status`, `nextRunAt`, `totalRuns`

### `generate schedule pause`

Pause an active FSD schedule without deleting it.

**Syntax:**

```bash
lgp generate schedule pause <scheduleId>
```

**Flags:**

| Flag | Type | Required | Default | Description |
|------|------|----------|---------|-------------|
| `<scheduleId>` | string (positional) | Yes | — | Schedule ID to pause |

**Example:**

```bash
python lgp.py generate schedule pause sched-uuid
```

### `generate schedule resume`

Resume a paused FSD schedule.

**Syntax:**

```bash
lgp generate schedule resume <scheduleId>
```

**Flags:**

| Flag | Type | Required | Default | Description |
|------|------|----------|---------|-------------|
| `<scheduleId>` | string (positional) | Yes | — | Schedule ID to resume |

**Example:**

```bash
python lgp.py generate schedule resume sched-uuid
```

### `generate schedule delete`

Delete an FSD schedule and cancel any pending runs.

**Syntax:**

```bash
lgp generate schedule delete <scheduleId>
```

**Flags:**

| Flag | Type | Required | Default | Description |
|------|------|----------|---------|-------------|
| `<scheduleId>` | string (positional) | Yes | — | Schedule ID to delete |

**Example:**

```bash
python lgp.py generate schedule delete sched-uuid
```

---

## admin

Manage platform administration: companies, members, users, views, clients, and organisation trees. All admin commands require the `LGP_ADMIN_KEY` environment variable to be set. The CLI sends `X-Admin-Key` header on every admin request for authentication.

> **Note:** The admin CLI uses Python (`lgp.py`) and communicates with `/api/admin/*` endpoints. All commands support the `--json` flag for raw JSON output.

### Environment Variables

| Variable | Description | Required |
|----------|-------------|----------|
| `LGP_ADMIN_KEY` | Admin secret key for `X-Admin-Key` header | Yes (for all admin commands) |

If `LGP_ADMIN_KEY` is not set, the CLI exits with an error message instructing the user to set the variable.

---

### `admin companies list`

List all companies with owner details, member counts, and client counts.

**Syntax:**

```bash
lgp admin companies list [--json]
```

**Flags:**

| Flag | Type | Required | Default | Description |
|------|------|----------|---------|-------------|
| `--json` | boolean | No | `false` | Output raw JSON instead of formatted table |

**Table columns:** `ID`, `Name`, `Owner Email`, `Members`, `Clients`, `Created`

**Example:**

```bash
lgp admin companies list
```

**Expected output (table):**

```
ID                                    Name           Owner Email          Members  Clients  Created
------------------------------------  -------------  -------------------  -------  -------  ----------
company-abc123                        Acme Corp      [REDACTED_SECRET]       5        3        2026-01-15
company-def456                        Beta Inc       [REDACTED_SECRET]        2        1        2026-02-01
```

---

### `admin companies show`

Show detailed information for a specific company including members and data counts.

**Syntax:**

```bash
lgp admin companies show --id <companyId> [--json]
```

**Flags:**

| Flag | Type | Required | Default | Description |
|------|------|----------|---------|-------------|
| `--id <companyId>` | string | Yes | — | Company ID to show details for |
| `--json` | boolean | No | `false` | Output raw JSON instead of formatted table |

**Example:**

```bash
lgp admin companies show --id company-abc123
```

---

### `admin companies members list`

List all members of a specific company.

**Syntax:**

```bash
lgp admin companies members list --company <id> [--json]
```

**Flags:**

| Flag | Type | Required | Default | Description |
|------|------|----------|---------|-------------|
| `--company <id>` | string | Yes | — | Company ID to list members for |
| `--json` | boolean | No | `false` | Output raw JSON instead of formatted table |

**Example:**

```bash
lgp admin companies members list --company company-abc123
```

---

### `admin companies members add`

Add a new member to a company.

**Syntax:**

```bash
lgp admin companies members add --company <id> --email <email> --role <role> [--json]
```

**Flags:**

| Flag | Type | Required | Default | Description |
|------|------|----------|---------|-------------|
| `--company <id>` | string | Yes | — | Company ID to add the member to |
| `--email <email>` | string | Yes | — | Email address of the user to add |
| `--role <role>` | string | Yes | — | Role: `owner`, `admin`, `member`, `viewer` |
| `--json` | boolean | No | `false` | Output raw JSON instead of formatted output |

**Example:**

```bash
lgp admin companies members add --company company-abc123 --email [REDACTED_SECRET] --role member
```

---

### `admin companies members update`

Update a company member's role, client access, and organisation rights.

**Syntax:**

```bash
lgp admin companies members update --company <id> --user <userId> [options] [--json]
```

**Flags:**

| Flag | Type | Required | Default | Description |
|------|------|----------|---------|-------------|
| `--company <id>` | string | Yes | — | Company ID |
| `--user <userId>` | string | Yes | — | User ID of the member to update |
| `--role <role>` | string | No | — | New role: `owner`, `admin`, `member`, `viewer` |
| `--access-mode <mode>` | string | No | — | Client access mode: `all`, `own`, `specific` |
| `--client-ids <ids>` | string | No | — | Comma-separated client IDs (required when access-mode is `specific`) |
| `--group <group>` | string | No | — | Organisation group: `admin`, `manager`, `user`, `viewer` |
| `--menu-access <json>` | string | No | — | JSON array of menu keys (e.g., `'["dashboard","leads"]'`) |
| `--json` | boolean | No | `false` | Output raw JSON instead of formatted output |

**Example:**

```bash
lgp admin companies members update --company company-abc123 --user usr-456 \
  --role admin --access-mode specific --client-ids "client-1,client-2" --group manager
```

---

### `admin companies members remove`

Remove a member from a company. Cannot remove the company owner.

**Syntax:**

```bash
lgp admin companies members remove --company <id> --user <userId> [--json]
```

**Flags:**

| Flag | Type | Required | Default | Description |
|------|------|----------|---------|-------------|
| `--company <id>` | string | Yes | — | Company ID |
| `--user <userId>` | string | Yes | — | User ID of the member to remove |
| `--json` | boolean | No | `false` | Output raw JSON instead of formatted output |

**Example:**

```bash
lgp admin companies members remove --company company-abc123 --user usr-456
```

---

### `admin companies clients list`

List all clients belonging to a specific company.

**Syntax:**

```bash
lgp admin companies clients list --company <id> [--json]
```

**Flags:**

| Flag | Type | Required | Default | Description |
|------|------|----------|---------|-------------|
| `--company <id>` | string | Yes | — | Company ID to list clients for |
| `--json` | boolean | No | `false` | Output raw JSON instead of formatted table |

**Example:**

```bash
lgp admin companies clients list --company company-abc123
```

---

### `admin companies org-tree`

Display the organisation tree for a company showing members, roles, client access, and views.

**Syntax:**

```bash
lgp admin companies org-tree --company <id> [--detailed] [--json]
```

**Flags:**

| Flag | Type | Required | Default | Description |
|------|------|----------|---------|-------------|
| `--company <id>` | string | Yes | — | Company ID to display org tree for |
| `--detailed` | boolean | No | `false` | Show full view lists, client names, menuAccess, and permissions for each member |
| `--json` | boolean | No | `false` | Output raw JSON from the org-tree API instead of ASCII tree |

**Example (default):**

```bash
lgp admin companies org-tree --company company-abc123
```

**Expected output (default):**

```
Acme Corp
├── [REDACTED_SECRET] [owner, admin]
│   ├── Client Access: all
│   └── Views: 6 views
├── [REDACTED_SECRET] [member, user]
│   ├── Client Access: specific (2 clients)
│   └── Views: 3 views
└── [REDACTED_SECRET] [viewer, viewer]
    ├── Client Access: all
    └── Views: 1 view
```

**Example (detailed):**

```bash
lgp admin companies org-tree --company company-abc123 --detailed
```

**Expected output (detailed):**

```
Acme Corp
├── [REDACTED_SECRET] [owner, admin]
│   ├── Client Access: all
│   ├── Views: full, prod2, focus, marketing, sales, analyst
│   ├── Menu Access: (none)
│   └── Permissions: (none)
├── [REDACTED_SECRET] [member, user]
│   ├── Client Access: specific
│   │   ├── Q1 Campaign (client-abc)
│   │   └── Q2 Outbound (client-def)
│   ├── Views: prod2, focus
│   ├── Menu Access: dashboard, leads
│   └── Permissions: canExport=true, canDelete=false
└── [REDACTED_SECRET] [viewer, viewer]
    ├── Client Access: all
    ├── Views: prod2
    ├── Menu Access: (none)
    └── Permissions: (none)
```

---

### `admin users list`

List all Cognito users with roles and view assignments. Supports search filtering.

**Syntax:**

```bash
lgp admin users list [--search <term>] [--json]
```

**Flags:**

| Flag | Type | Required | Default | Description |
|------|------|----------|---------|-------------|
| `--search <term>` | string | No | — | Filter users by email substring (case-insensitive) |
| `--json` | boolean | No | `false` | Output raw JSON instead of formatted table |

**Table columns:** `Username`, `Email`, `Role`, `Views`, `Status`, `Master Admin`

**Example:**

```bash
lgp admin users list --search acme
```

---

### `admin users set-role`

Set a user's role in Cognito.

**Syntax:**

```bash
lgp admin users set-role --username <username> --role <role> [--json]
```

**Flags:**

| Flag | Type | Required | Default | Description |
|------|------|----------|---------|-------------|
| `--username <username>` | string | Yes | — | Cognito username |
| `--role <role>` | string | Yes | — | Role ID: `admin`, `companyAdmin`, `marketing`, `sales`, `analyst`, `viewer`, `custom` |
| `--json` | boolean | No | `false` | Output raw JSON instead of formatted output |

**Example:**

```bash
lgp admin users set-role --username user-abc123 --role marketing
```

---

### `admin users set-views`

Set a user's allowed views in Cognito.

**Syntax:**

```bash
lgp admin users set-views --username <username> --views <views> [--json]
```

**Flags:**

| Flag | Type | Required | Default | Description |
|------|------|----------|---------|-------------|
| `--username <username>` | string | Yes | — | Cognito username |
| `--views <views>` | string | Yes | — | Comma-separated view IDs (e.g., `full,prod2,focus`) |
| `--json` | boolean | No | `false` | Output raw JSON instead of formatted output |

**Example:**

```bash
lgp admin users set-views --username user-abc123 --views full,prod2,focus
```

---

### `admin views list`

List all available view configurations and role definitions.

**Syntax:**

```bash
lgp admin views list [--json]
```

**Flags:**

| Flag | Type | Required | Default | Description |
|------|------|----------|---------|-------------|
| `--json` | boolean | No | `false` | Output raw JSON instead of formatted tables |

**Example:**

```bash
lgp admin views list
```

---

### `admin clients list`

List all clients across all companies with company name resolution.

**Syntax:**

```bash
lgp admin clients list [--json]
```

**Flags:**

| Flag | Type | Required | Default | Description |
|------|------|----------|---------|-------------|
| `--json` | boolean | No | `false` | Output raw JSON instead of formatted table |

**Example:**

```bash
lgp admin clients list
```


---

### `admin backup tables`

List all DynamoDB tables, optionally filtered by name prefix. Useful for discovering Amplify-managed tables before creating backups.

**Syntax:**

```bash
lgp admin backup tables [--prefix PREFIX] [--json]
```

**Flags:**

| Flag | Type | Required | Default | Description |
|------|------|----------|---------|-------------|
| `--prefix <PREFIX>` | string | No | — | Filter table names by prefix string |
| `--json` | boolean | No | `false` | Output raw JSON instead of formatted table |

**Example:**

```bash
lgp admin backup tables
lgp admin backup tables --prefix Company
lgp admin backup tables --json
```

**Expected output (table):**

```
Table Name
------------------------------------
Company-abc123def
CompanyUser-abc123def
Client-abc123def
EnrichLeads-abc123def
SourceLeads-abc123def
...
Found 25 tables
```

**Expected output (json):**

```json
{
  "tables": ["Company-abc123def", "CompanyUser-abc123def", "Client-abc123def"],
  "count": 3
}
```

---

### `admin backup create`

Create an on-demand backup for a single table or all discovered tables.

**Syntax:**

```bash
lgp admin backup create --table <TABLE> [--json]
lgp admin backup create --all [--json]
```

**Flags:**

| Flag | Type | Required | Default | Description |
|------|------|----------|---------|-------------|
| `--table <TABLE>` | string | One of `--table` or `--all` | — | DynamoDB table name to back up |
| `--all` | boolean | One of `--table` or `--all` | `false` | Back up all discovered tables |
| `--json` | boolean | No | `false` | Output raw JSON instead of formatted output |

**Backup naming convention:** `<tableName>-<YYYYMMDD>-<HHmmss>` (UTC)

**Example — single table:**

```bash
lgp admin backup create --table Company-abc123def
```

**Expected output (single table):**

```json
{
  "backup": {
    "backupArn": "arn:aws:dynamodb:us-east-1:123456789012:table/Company-abc123def/backup/01234567890123-abcdef",
    "tableName": "Company-abc123def",
    "backupName": [REDACTED_SECRET],
    "backupStatus": "CREATING",
    "createdAt": "2026-03-15T14:30:22.000Z"
  }
}
```

**Example — all tables:**

```bash
lgp admin backup create --all
```

**Expected output (all tables, table format):**

```
Creating backups for 25 tables...
✓ Company-abc123          Company-abc123-20260315-143022
✓ CompanyUser-abc123      CompanyUser-abc123-20260315-143023
✗ SomeTable-xyz           Table not found
...
Summary: 24 succeeded, 1 failed
```

---

### `admin backup list`

List existing backups with optional table name and status filters.

**Syntax:**

```bash
lgp admin backup list [--table TABLE] [--status STATUS] [--json]
```

**Flags:**

| Flag | Type | Required | Default | Description |
|------|------|----------|---------|-------------|
| `--table <TABLE>` | string | No | — | Filter backups by table name |
| `--status <STATUS>` | string | No | — | Filter by status: `AVAILABLE`, `CREATING`, `DELETED` |
| `--json` | boolean | No | `false` | Output raw JSON instead of formatted table |

**Example:**

```bash
lgp admin backup list
lgp admin backup list --table Company-abc123def --status AVAILABLE
lgp admin backup list --json
```

**Expected output (table):**

```
ARN (truncated)          Table              Status     Size       Created
-----------------------  -----------------  ---------  ---------  ----------
...abc123/backup-001     Company-abc123     AVAILABLE  1.0 MB     2026-03-15
...abc123/backup-002     CompanyUser-abc    AVAILABLE  512.0 KB   2026-03-15
```

---

### `admin backup describe`

Get detailed information about a specific backup by ARN.

**Syntax:**

```bash
lgp admin backup describe --arn <ARN> [--json]
```

**Flags:**

| Flag | Type | Required | Default | Description |
|------|------|----------|---------|-------------|
| `--arn <ARN>` | string | Yes | — | Full backup ARN |
| `--json` | boolean | No | `false` | Output raw JSON instead of key-value pairs |

**Example:**

```bash
lgp admin backup describe --arn arn:aws:dynamodb:us-east-1:123456789012:table/Company-abc123/backup/01234567890123-abcdef
```

**Expected output (json):**

```json
{
  "backup": {
    "backupArn": "arn:aws:dynamodb:us-east-1:...",
    "tableName": "Company-abc123",
    "tableId": "abc123-def456",
    "backupStatus": "AVAILABLE",
    "backupType": "USER",
    "backupSizeBytes": 1048576,
    "itemCount": 5000,
    "createdAt": "2026-03-15T14:30:22.000Z",
    "expiryDate": null
  }
}
```

---

### `admin backup delete`

Delete a specific backup by ARN.

**Syntax:**

```bash
lgp admin backup delete --arn <ARN> [--json]
```

**Flags:**

| Flag | Type | Required | Default | Description |
|------|------|----------|---------|-------------|
| `--arn <ARN>` | string | Yes | — | Full backup ARN to delete |
| `--json` | boolean | No | `false` | Output raw JSON instead of formatted confirmation |

**Example:**

```bash
lgp admin backup delete --arn arn:aws:dynamodb:us-east-1:123456789012:table/Company-abc123/backup/01234567890123-abcdef
```

**Expected output (json):**

```json
{
  "deleted": {
    "backupArn": "arn:aws:dynamodb:us-east-1:...",
    "tableName": "Company-abc123"
  }
}
```

**Error cases:**
- Backup not found → 404
- Backup still in CREATING status → 400 "Cannot delete a backup that is still being created"

---

### `admin backup restore`

Restore a table from a backup to a new target table name.

**Syntax:**

```bash
lgp admin backup restore --arn <ARN> --target-table <NAME> [--json]
```

**Flags:**

| Flag | Type | Required | Default | Description |
|------|------|----------|---------|-------------|
| `--arn <ARN>` | string | Yes | — | Full backup ARN to restore from |
| `--target-table <NAME>` | string | Yes | — | Name for the restored table (must not already exist) |
| `--json` | boolean | No | `false` | Output raw JSON instead of formatted output |

**Example:**

```bash
lgp admin backup restore \
  --arn arn:aws:dynamodb:us-east-1:123456789012:table/Company-abc123/backup/01234567890123-abcdef \
  --target-table Company-abc123-restored-20260315
```

**Expected output (json):**

```json
{
  "restored": {
    "targetTableName": [REDACTED_SECRET],
    "tableArn": "arn:aws:dynamodb:us-east-1:...",
    "tableStatus": "CREATING",
    "sourceBackupArn": "arn:aws:dynamodb:us-east-1:..."
  }
}
```

**Error cases:**
- Backup not found → 404
- Target table already exists → 409

---

### `admin backup pitr-status`

Check Point-in-Time Recovery (PITR) status for one or all tables.

**Syntax:**

```bash
lgp admin backup pitr-status [--table TABLE] [--json]
```

**Flags:**

| Flag | Type | Required | Default | Description |
|------|------|----------|---------|-------------|
| `--table <TABLE>` | string | No | — | Specific table to check; if omitted, checks all tables |
| `--json` | boolean | No | `false` | Output raw JSON instead of formatted table |

**Example:**

```bash
lgp admin backup pitr-status
lgp admin backup pitr-status --table Company-abc123def
lgp admin backup pitr-status --json
```

**Expected output (table):**

```
Table Name               PITR     Earliest Restore     Latest Restore
-----------------------  -------  -------------------  -------------------
Company-abc123           Enabled  2026-03-10 00:00     2026-03-15 14:30
CompanyUser-abc123       Disabled -                    -
```

---

### `admin backup pitr-enable`

Enable Point-in-Time Recovery on a specific table.

**Syntax:**

```bash
lgp admin backup pitr-enable --table <TABLE>
```

**Flags:**

| Flag | Type | Required | Default | Description |
|------|------|----------|---------|-------------|
| `--table <TABLE>` | string | Yes | — | DynamoDB table name to enable PITR on |

**Example:**

```bash
lgp admin backup pitr-enable --table Company-abc123def
```

**Expected output:**

```json
{
  "tableName": "Company-abc123def",
  "pitrEnabled": true,
  "message": "PITR enabled successfully"
}
```

---

### `admin backup pitr-disable`

Disable Point-in-Time Recovery on a specific table.

**Syntax:**

```bash
lgp admin backup pitr-disable --table <TABLE>
```

**Flags:**

| Flag | Type | Required | Default | Description |
|------|------|----------|---------|-------------|
| `--table <TABLE>` | string | Yes | — | DynamoDB table name to disable PITR on |

**Example:**

```bash
lgp admin backup pitr-disable --table Company-abc123def
```

**Expected output:**

```json
{
  "tableName": "Company-abc123def",
  "pitrEnabled": false,
  "message": "PITR disabled successfully"
}
```


---

## account-analysis

Company-level lead analytics: group leads by company, compute aggregate metrics, and export results. All subcommands default to `--format table` (unlike the global `json` default).

### `account-analysis list`

List company groups with sorting and filtering.

**Syntax:**

```bash
lgp account-analysis list --client <ID> [options]
```

**Flags:**

| Flag | Type | Required | Default | Description |
|------|------|----------|---------|-------------|
| `--client <ID>` | string | Yes | — | Client ID to analyze |
| `--sort <field>` | string | No | `count` | Sort field: `count`, `avg_score`, `total_score` |
| `--order <dir>` | string | No | `desc` | Sort direction: `asc`, `desc` |
| `--limit <n>` | integer | No | all | Return top N companies |
| `--min-leads <n>` | integer | No | — | Exclude companies with fewer leads |
| `--format <fmt>` | string | No | `table` | Output format: `json` or `table` |
| `--no-cache` | boolean | No | `false` | Bypass local cache |
| `--refresh` | boolean | No | `false` | Invalidate cache and fetch fresh data |

**Table columns:** Company Name, Domain, Lead Count, Avg Score, Total Score

**Example:**

```bash
python lgp.py account-analysis list \
  --client cl_9f3a2b7e \
  --sort avg_score \
  --order desc \
  --min-leads 3
```

---

### `account-analysis analyze`

Detailed metrics for all companies or a single company.

**Syntax:**

```bash
lgp account-analysis analyze --client <ID> [options]
```

**Flags:**

| Flag | Type | Required | Default | Description |
|------|------|----------|---------|-------------|
| `--client <ID>` | string | Yes | — | Client ID to analyze |
| `--company <name>` | string | No | — | Specific company name (case-insensitive) |
| `--limit <n>` | integer | No | all | Return top N companies |
| `--format <fmt>` | string | No | `table` | Output format: `json` or `table` |
| `--no-cache` | boolean | No | `false` | Bypass local cache |
| `--refresh` | boolean | No | `false` | Invalidate cache and fetch fresh data |

**Without `--company`:** Table with columns: Company, Leads, Avg Score, Median, Max, Min, Velocity, Champion

**With `--company`:** Detailed key-value format with score stats, status distribution percentages, engagement velocity, and champion lead details.

**Example — all companies:**

```bash
python lgp.py account-analysis analyze --client cl_9f3a2b7e
```

**Example — single company:**

```bash
python lgp.py account-analysis analyze \
  --client cl_9f3a2b7e \
  --company "Acme Corp"
```

---

### `account-analysis export`

Export analysis results to CSV or JSON file (or stdout).

**Syntax:**

```bash
lgp account-analysis export --client <ID> --format <csv|json> [options]
```

**Flags:**

| Flag | Type | Required | Default | Description |
|------|------|----------|---------|-------------|
| `--client <ID>` | string | Yes | — | Client ID to analyze |
| `--format <fmt>` | string | Yes | — | Export format: `csv` or `json` |
| `--output <path>` | string | No | stdout | File path to write output to |
| `--no-cache` | boolean | No | `false` | Bypass local cache |
| `--refresh` | boolean | No | `false` | Invalidate cache and fetch fresh data |

**Example — export CSV to file:**

```bash
python lgp.py account-analysis export \
  --client cl_9f3a2b7e \
  --format csv \
  --output ./company-analysis.csv
```

**Example — export JSON to stdout:**

```bash
python lgp.py account-analysis export \
  --client cl_9f3a2b7e \
  --format json
```

---

### `account-analysis cache-clear`

Clear the local ABLA cache for a specific client or all clients.

**Syntax:**

```bash
lgp account-analysis cache-clear [options]
```

**Flags:**

| Flag | Type | Required | Default | Description |
|------|------|----------|---------|-------------|
| `--client <ID>` | string | No | — | Clear cache for a specific client (omit to clear all) |

**Example:**

```bash
python lgp.py account-analysis cache-clear --client cl_9f3a2b7e
```

**Cache details:**
- Location: `~/.leadgenius/abla-cache.json`
- TTL: 15 minutes
- Use `--no-cache` on any subcommand to bypass cache for a single request
- Use `--refresh` to invalidate and re-fetch


Security scan verdicts — ClawScan: suspicious | static analysis: suspicious | VirusTotal: clean | SkillSpector: suspicious (CRITICAL)