# Personalisation Line Generator (`cloudastra-technologies/personalisation-line-generator`) Actor

One factual, campaign-ready opening line per company, grounded in that company's own website and quoted back to you. Lines we cannot ground come back as HOLD, and HOLD is free.

- **URL**: https://apify.com/cloudastra-technologies/personalisation-line-generator.md
- **Developed by:** [Cloudastra Technologies](https://apify.com/cloudastra-technologies) (community)
- **Categories:** Lead generation, Marketing, AI
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $5.00 / 1,000 opening lines

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.

Learn more: https://docs.apify.com/actors/running/actors-in-store.md#pay-per-event

## What's an Apify Actor?

An Actor is a serverless cloud program that runs on the Apify platform. It has two run modes.
In Batch mode, an Actor accepts a well-defined JSON input, performs an action which can take anything from a few seconds to a few hours,
and optionally produces a well-defined JSON output, datasets with results, or files in key-value store.
In Standby mode, an Actor provides a web server which can be used as a website, API, or an MCP server.

Apify vocabulary and the platform model are defined once, in the agent quickstart at https://apify.com/agents.md.

## How to integrate an Actor?

If asked about integration, you help developers integrate Actors into their projects.
You adapt to their stack and deliver integrations that are safe, well-documented, and production-ready.

Do not guess an integration path. Every one of them is in the agent quickstart at https://apify.com/agents.md: the Apify MCP server, Agent Skills with the Apify CLI, the JavaScript and Python clients, the REST API, and the account-free path for an agent with no human to sign in. It also carries the rule on stating cost before the first paid run.

For examples already wired to this Actor's own input schema, see the [API](#api) section below.

Each client library has reference documentation the quickstart does not restate: [JavaScript/TypeScript](https://docs.apify.com/api/client/js/docs.md) (`npm install apify-client`) and [Python](https://docs.apify.com/api/client/python/docs.md) (`pip install apify-client`).

# README

## Personalisation Line Generator

**Give it a list of company domains. Get back one factual opening line per
company, grounded in that company's own website, with the quote that proves it.**

When we cannot ground a line, you get an honest HOLD instead of a generic
sentence. You are not charged for those.

***

### What you get

Feed it five domains:

```json
{
  "domains": [
    "monzo.com",
    "gocardless.com",
    "starlingbank.com",
    "wise.com",
    "some-dead-domain.example"
  ],
  "style": "observation",
  "maxWords": 18
}
```

You get one row per company, ready to paste into your sending tool:

| domain | status | line | evidence |
|---|---|---|---|
| monzo.com | OK | found that you build current accounts and lending entirely inside a mobile app | quote from `/about` |
| gocardless.com | OK | found that you handle recurring bank payments and Direct Debit for subscription businesses | quote from `/features` |
| starlingbank.com | OK | found that you run business current accounts with built in bookkeeping tools | quote from `/business` |
| wise.com | OK | found that you move money across borders at the mid market exchange rate | quote from `/pricing` |
| some-dead-domain.example | **HOLD** (`site_unreachable`) | | |

Four usable lines. One honest blank. **You pay for four.**

Every OK row carries `evidence_url` and `evidence_quote`, so you can click
through and check our work in about five seconds. If a line is wrong, you can
see exactly which sentence on their site produced it.

### Who this is for

- **Cold email teams** who want a real first line per prospect instead of a
  merge field
- **Agencies** running outbound for several clients at once
- **Founders** doing their own outreach who do not have time to read 300 websites
- **Anyone with a lead list** who already knows that a generic opener gets ignored

If you are sending to companies and you want the first sentence to prove you
looked, this is for you.

### How it works

```
YOUR DOMAINS
    |
    v
1. FETCH      We read the company's homepage and up to two internal pages.
              If that is not enough to say what the company does, we run one
              web search limited to the company's own site and read up to two
              more of its pages. Search results are only used to find pages;
              the quote always comes from a page we read ourselves. robots.txt
              is respected. Sites that do not respond, or that still render
              nothing without JavaScript, are held rather than guessed at.
    |
    v
2. EXTRACT    We pull out what the company actually does: its service phrases,
              the city it operates in, the language the site is written in, and
              whether this is a company at all rather than a directory listing.
    |
    v
3. GENERATE   A language model writes one line from those extracted phrases.
              It never sees the raw page, only the extracted text, and it has
              to tell us which snippet it used. That snippet becomes your
              evidence quote.
    |
    v
4. CHECK      Enforced in code, not asked for in a prompt: the word cap, no
              em or en dashes, nothing negative about the company, and a check
              that the evidence actually supports the line. A line that fails
              is regenerated once, then held.
    |
    v
5. DEDUPE     Across your whole batch, no two lines may read alike. A collision
              is regenerated once, then held as duplicate_risk, naming the
              company it collided with.
```

### What makes it different

**1. Grounded or held.** Every line comes with the sentence from the company's
own website that supports it, and the URL it came from. If we cannot find that
sentence, we do not write the line. Generating from a company name and a domain
is what produces the openers that read like mail merge, and it is the failure
mode we built this to avoid.

**2. Batch unique.** No two lines in your run may read alike. This sounds
obvious until you have sent a campaign where most of the list received the same
sentence with the company name swapped in. Recipients compare notes, and the
whole batch reads as automated the moment two of them do. The deduplication
runs across the entire run, not just neighbouring rows.

**3. You pay only for usable lines.** OK lines are charged. HOLD rows are free.
Not discounted, free. That is the whole pricing model, and it is deliberate: if
we charged for HOLDs we would have a reason to ship a weak line rather than hold
it, and you would have to trust that we did not. This way our incentive and
yours point the same direction.

### Pricing

**$5 per 1,000 OK lines, plus a start fee of $0.00005 per run. HOLD rows
cost nothing.**

- A line you can use is charged. A blank with a stated reason is not
- Duplicate domains in your input are collapsed before the run starts, so the
  same company is never charged twice
- **Try it first:** turn on `preview` to process only your first 10 domains. A
  first run costs at most about $0.05. It uses the identical engine, the same
  rules and the same evidence, so what you see is what you get. It is a spend
  cap rather than a free tier

Most tools in this category sell you a subscription plus a bundle of credits
that expire whether you use them or not, and charge you per line whether the
line is usable or not. Here there is nothing to sign up for, nothing expires,
and unusable lines are free. That is the comparison that matters, and it is why
this exists as an actor instead of a SaaS product.

### Chains with

This actor takes domains and returns lines, so it drops into the middle of
almost any list building workflow:

- Any company or lead scraper on the store that outputs a website column
- **Lead List Cleaner** (ours) to deduplicate and normalise a multi source list
  before you spend anything generating lines for the same company twice
- **Company Lookalike Finder** (ours) to build the list in the first place, then
  personalise it here
- Your sending tool, by exporting to CSV or JSON

The natural order is find companies, clean the list, then write the lines.

### Limitations, in plain language

Read this before you buy. It is here so you do not need a refund.

**HOLD is a feature, not a failure.** A generic line burns your reply rate and
costs you the prospect. An honest blank costs you nothing and tells you to skip
that row or write it yourself. We would rather return 70 good lines and 30
blanks than 100 lines you have to read one by one to find the bad ones. If you
want a tool that always produces something, this is not it, and that is on
purpose.

**JavaScript only sites cannot be read.** We do not run a browser. A site that
renders nothing without executing JavaScript comes back as `js_only`. This is a
real gap and it is more common on small business sites built with page builders.

**Some sites tell us not to read them, and we listen.** If a site's
`robots.txt` asks automated readers to stay out, we do, and the row comes back
as `robots_blocked`. Re-running will not change it. We could ignore that file
and most tools do, but an actor that crawls sites which asked it not to gets
reported and delisted, and then it is not there for you either.

**Non English sites are held, not translated.** By default we accept English
only, and a site in another language comes back as `wrong_language` with the
language we detected. You can widen this with the `languages` input, but the
line will be written in that language, and our own testing is strongest in
English.

**Thin sites cannot support a specific line.** A one page brochure site that
says "quality service since 1998" genuinely does not contain anything specific
enough to write about. Those come back as `too_thin`. We are not going to invent
a detail to fill the gap.

**Your list quality decides your HOLD rate.** A list of real companies with real
websites holds very little. A scraped list full of directory pages, parked
domains and dead sites holds a lot, and every one of those is free but also
means one fewer usable line. Cleaning the list first is worth doing.

**This describes companies, not people.** No names, no job titles, no emails,
no personal details of any kind. The line is about what the company does. If you
need contacts, chain this with a contact finder.

**We do not scrape LinkedIn.** Not as a source, not as a fallback. This keeps
the actor stable and keeps you clear of the takedown risk that follows tools
which do.

**One style.** Every line is an observation about what the company does,
grounded in a sentence from its own site. A question style was tried and
removed before launch: asked as questions, the lines came out as asking a
company how it does the very thing it sells.

**`customContext` steers the angle, it never adds a claim.** Telling us what you
sell makes us more likely to lead with the relevant true thing about a company.
It cannot make us write something their site does not say. If you describe an
offer that has nothing to do with a company, you get a line about what that
company actually does, or a HOLD.

### Input

| Field | Required | Description |
|---|---|---|
| `domains` | **yes** | 1 to 60 company domains per run. Split a longer list across runs |
| `style` | no | `observation`, the only style |
| `maxWords` | no | 5 to 40, default 18. Hard cap, enforced after generation |
| `customContext` | no | Up to 300 characters on what you are reaching out about. Tilts the angle, never adds a claim |
| `languages` | no | ISO 639-1 codes, default `["en"]`. Other languages are held |
| `preview` | no | Process only the first 10 domains, capping a first run at about $0.05 |

### Output

One row per input domain, whether it produced a line or not:
`domain`, `status`, `line`, `hold_reason`, `evidence_url`, `evidence_quote`,
`matched_phrases`, `words`, `style`, `language`, `hold_detail`,
`final_domain`, `pages_fetched`, `generatedAt`.

`status` is `OK` or `HOLD`. On a HOLD, `line` is empty and `hold_reason` is one
of seven documented values: `site_unreachable`, `robots_blocked`, `js_only`,
`too_thin`, `non_company`, `duplicate_risk`, `wrong_language`.

`hold_detail` is one sentence saying why a row was held. A duplicate line
names the company it read too much like, so you can see the pair for yourself.

`final_domain` is where the site ended up after redirects. It usually matches
`domain`. When it does not, the company we described is not the one on your list
row, and that is worth a look before you send.

Rows are pushed as they are generated, so if you stop a run early you keep
everything produced so far. Export to CSV, JSON or Excel, or pipe it straight
into another actor.

### FAQ

**What HOLD rate should I expect?**
It depends almost entirely on your list. A clean list of real companies with
working sites holds very little. A raw scraped list holds a lot. Every HOLD is
free, so a bad list costs you nothing except the lines you did not get.

**Do I need an API key for anything?**
No. Not for us, and not for the language model either. Some tools in this
category ask you to bring your own paid model key and pay that bill separately.
Everything here is included in the per line price.

**Can I use the lines as they are?**
Yes, that is the point. Every OK line is capped at your word limit, contains no
em or en dashes, and says something the company says about itself. Read the
evidence quote if you want to check one before sending.

**Why did one company get held when its site looks fine to me?**
Open the row and read `hold_reason`. The usual answers are that the site needs
JavaScript to show its text, or that it says very little in words a machine can
read, or that its line was too close to another line in the same run.

**One row came back describing a different company. Why?**
Check `final_domain`. Some domains redirect to a parent or group site, so the
page we read genuinely belonged to that company rather than the one on your row.
We record both instead of guessing which one you meant.

**Can I run the same list twice and get different lines?**
Yes, the wording can change a little. A language model writes each line, so a
second run over the same domain can phrase it differently, and now and then a
domain that got a line once is held the next time, or the other way round.
What does not change is the rule: every line still has to be supported by a
quote from that company's own site, and you are still charged only for the
lines you get.

**Can I schedule it?**
You can, but this is a batch tool rather than a monitor. Run it when you have a
new list.

**What if a line is wrong?**
Send us the domain and the line. The evidence quote makes these fast to diagnose,
because we can see exactly which sentence produced it. A line that its own
evidence does not support is a bug we want to know about.

### Support

Issues and questions through the Apify listing. We aim to acknowledge within
24 hours and to fix genuine breakage within 72 hours.

The actor runs an automated health check every day, so breakage is usually found
and fixed before you would notice it.

# Actor input Schema

## `domains` (type: `array`):

1 to 60 companies, as domains (example.com). Full URLs work too, we strip them down. One line is produced per domain. Duplicate domains are collapsed before the run starts, so you are never charged twice for the same company.

## `style` (type: `string`):

The sentence shape. One style is offered: an observation about what the company does, grounded in a sentence from its own website.

## `maxWords` (type: `integer`):

5 to 40 words, default 18. This is a hard cap enforced after generation, not a suggestion to the model: a line over the cap is regenerated once and then held. Short caps raise the HOLD rate, because some companies cannot be described specifically in eight words.

## `customContext` (type: `string`):

Optional, up to 300 characters. One sentence about your own offer, used only to choose which true thing about the company to lead with. It tilts the angle, it never becomes a claim: nothing you write here can be asserted about the company, and every line is still grounded in a quote from their own site. Example: "we sell compliance software to healthcare providers" makes a line about their NHS work more likely than a line about their office move.

## `languages` (type: `array`):

ISO 639-1 codes. A company whose site is not in one of these languages is held as wrong_language rather than translated, because a line written from a language we did not read is a line we cannot stand behind. Default is English only. Add codes to widen it.

## `preview` (type: `boolean`):

Judge the quality on your own domains before committing. Processes only the first 10 domains, so a first run costs at most about $0.05. Same engine, same evidence, same rules, just fewer rows. This is a spend cap, not a free tier.

## Actor input object example

```json
{
  "domains": [
    "monzo.com",
    "gocardless.com",
    "starlingbank.com"
  ],
  "style": "observation",
  "maxWords": 18,
  "languages": [
    "en"
  ],
  "preview": false
}
```

# Actor output Schema

## `results` (type: `string`):

One row per domain. OK rows carry the opening line, the page it is grounded in, and the sentence from that page it rests on. HOLD rows carry the reason no line was written instead. Only OK rows are billed.

## `runSummary` (type: `string`):

What the run did: lines written, rows held and why, and whether the run stopped early on a limit. Not billed.

# API

You can run this Actor programmatically using our API. Below are code examples in JavaScript, Python, and CLI, as well as the OpenAPI specification and MCP server setup.

## JavaScript example

```javascript
import { ApifyClient } from 'apify-client';

// Initialize the ApifyClient with your Apify API token
// Replace the '<YOUR_API_TOKEN>' with your token
const client = new ApifyClient({
    token: '<YOUR_API_TOKEN>',
});

// Prepare Actor input
const input = {
    "domains": [
        "monzo.com",
        "gocardless.com",
        "starlingbank.com"
    ],
    "languages": [
        "en"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("cloudastra-technologies/personalisation-line-generator").call(input);

// Fetch and print Actor results from the run's dataset (if any)
console.log('Results from dataset');
console.log(`💾 Check your data here: https://console.apify.com/storage/datasets/${run.defaultDatasetId}`);
const { items } = await client.dataset(run.defaultDatasetId).listItems();
items.forEach((item) => {
    console.dir(item);
});

// 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/js/docs

```

## Python example

```python
from apify_client import ApifyClient

# Initialize the ApifyClient with your Apify API token
# Replace '<YOUR_API_TOKEN>' with your token.
client = ApifyClient("<YOUR_API_TOKEN>")

# Prepare the Actor input
run_input = {
    "domains": [
        "monzo.com",
        "gocardless.com",
        "starlingbank.com",
    ],
    "languages": ["en"],
}

# Run the Actor and wait for it to finish
run = client.actor("cloudastra-technologies/personalisation-line-generator").call(run_input=run_input)

# Fetch and print Actor results from the run's dataset (if there are any)
print(f"💾 Check your data here: https://console.apify.com/storage/datasets/{run.default_dataset_id}")
for item in client.dataset(run.default_dataset_id).iterate_items():
    print(item)

# 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/python/docs/quick-start

```

## CLI example

```bash
echo '{
  "domains": [
    "monzo.com",
    "gocardless.com",
    "starlingbank.com"
  ],
  "languages": [
    "en"
  ]
}' |
apify call cloudastra-technologies/personalisation-line-generator --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,cloudastra-technologies/personalisation-line-generator"
        }
    }
}
```

The hosted server signs you in with OAuth on first connect, so no API token belongs in this config. Clients without OAuth support can send an `Authorization: Bearer <APIFY_API_TOKEN>` header instead, using a token from API & Integrations in Apify Console (https://console.apify.com/settings/integrations).

## OpenAPI specification

Download the OpenAPI definition: https://api.apify.com/v2/actors/vYR3fW5LuBeUrnrk1/builds/3RTZ944s5gAI0o1oL/openapi.json
