Example actors
Two actors in the Store are small enough to read in full: lantfly~hello, a smoke test, and lantfly~web-scraper, a generic HTML scraper on Crawlee.
hello
lantfly~hello pushes count items to its dataset. Use it to check that runs, storages, webhooks and integrations work end to end. It is free to run.
Input
| Field | Type | Default | Limits |
|---|---|---|---|
message | string | Hello from Lantfly | |
count | integer | 3 | 1–1000 |
Default options: 256 MB memory, 120 seconds timeout.
Code
import { Actor } from '@lantfly/actor-sdk';
interface Input {
message?: string;
count?: number;
}
await Actor.main(async () => {
const { message = 'Hello from Lantfly', count = 3 } = await Actor.getInput<Input>();
Actor.log.info('Hello actor started', { count });
for (let index = 1; index <= count; index++) {
await Actor.pushData({ index, message, at: new Date().toISOString() });
}
await Actor.setValue('OUTPUT', { message, count });
Actor.log.info(`Pushed ${count} items`);
});
What it shows:
Actor.mainwraps the work: it initializes the SDK, and at the end flushes the items and exits with code 0.Actor.getInputreads theINPUTrecord. The platform already filled in the schema defaults; the code defaults only matter for local runs.Actor.pushDatabuffers items and sends them in batches.Actor.setValue('OUTPUT', …)writes its own summary. Because the actor never reports targets, the SDK keeps thisOUTPUTas it is.
Run it
lantfly call lantfly~hello --input '{"message": "Hi", "count": 2}' --output-dataset
Output items:
[
{ "index": 1, "message": "Hi", "at": "2026-11-03T08:00:00.000Z" },
{ "index": 2, "message": "Hi", "at": "2026-11-03T08:00:00.010Z" }
]
web-scraper
lantfly~web-scraper crawls pages over plain HTTP with Crawlee's CheerioCrawler and extracts fields with CSS selectors. Each page gives one item: url, the fields you map, and loadedAt. It does not run JavaScript on the pages, so use it for server-rendered HTML.
Input
| Field | Type | Default | Meaning |
|---|---|---|---|
startUrls | array of { "url": … } | required | Pages to start from. |
linkSelector | string | CSS selector of links to follow, for example a.next. | |
globs | array of strings | [] | Only follow links whose URL matches one of these globs. * matches anything but /, ** matches anything. |
fields | object | {"title": "title"} | Field name → CSS selector. End the selector with @attr to read an attribute (a.next@href, meta[name=description]@content). |
maxPagesPerCrawl | integer | 50 | Stop after this many pages. |
maxConcurrency | integer | 5 | Pages fetched at the same time (at most 50). |
proxyConfiguration | object | {"useLantflyProxy": false} | Proxy settings. |
Default options: 1024 MB memory, 3600 seconds timeout. Priced per result item.
Field values:
- A selector that matches one node gives a string; several nodes give a list; none gives
null. - Text is trimmed and whitespace collapsed.
@hrefand@srcvalues are turned into absolute URLs.
Code
import { Actor } from '@lantfly/actor-sdk'; // first: sets Crawlee's memory and storage dir
import { CheerioCrawler, ProxyConfiguration } from '@crawlee/cheerio';
import { extractFields, globToRegExp } from './extract.js';
interface Input {
startUrls: { url: string }[];
linkSelector?: string;
globs?: string[];
fields?: Record<string, string>;
maxPagesPerCrawl?: number;
maxConcurrency?: number;
proxyConfiguration?: { useLantflyProxy?: boolean; groups?: string[]; country?: string };
}
await Actor.main(async () => {
const input = await Actor.getInput<Input>();
const fields = input.fields ?? { title: 'title' };
const globs = (input.globs ?? []).map(globToRegExp);
const lantflyProxy = Actor.createProxyConfiguration(input.proxyConfiguration ?? { useLantflyProxy: false });
const crawler = new CheerioCrawler({
maxRequestsPerCrawl: input.maxPagesPerCrawl ?? 50,
maxConcurrency: input.maxConcurrency ?? 5,
proxyConfiguration: lantflyProxy
? new ProxyConfiguration({ newUrlFunction: (sessionId) => lantflyProxy.newUrl(sessionId) })
: undefined,
async requestHandler({ request, $, enqueueLinks }) {
const url = request.loadedUrl ?? request.url;
await Actor.pushData({ url, ...extractFields($, fields, url), loadedAt: new Date().toISOString() });
if (input.linkSelector) {
await enqueueLinks({
selector: input.linkSelector,
transformRequestFunction: (req) => (globs.length === 0 || globs.some((g) => g.test(req.url)) ? req : false),
});
}
},
failedRequestHandler({ request }, error) {
Actor.log.warning(`Failed ${request.url}: ${error.message}`);
},
});
await crawler.run((input.startUrls ?? []).map((s) => s.url));
Actor.log.info('Crawl finished');
});
What it shows:
- Import order. The SDK is imported before Crawlee, so Crawlee sizes its pool from the run's memory and keeps its own storage in
/tmp/crawlee. - Proxy from the input.
createProxyConfigurationtakes the input'sproxyConfigurationobject and returnsnullwhenuseLantflyProxyisfalse. Crawlee gets the Lantfly proxy throughnewUrlFunction, so each Crawlee session keeps its own sticky exit IP. - Crawlee's request queue. Links are queued with
enqueueLinks; the queue lives in the container. - Failures. A page that fails after Crawlee's retries is logged as a warning and the crawl goes on.
- Outbound guard. The real actor also checks every URL and redirect hop against public addresses before it fetches (left out above).
The Dockerfile is a two-stage Node.js 22 image with tini, procps (Crawlee's browser pool needs ps) and CRAWLEE_STORAGE_DIR=/tmp/crawlee, running as the node user. See Actor definition.
Run it
{
"startUrls": [{ "url": "https://example.com" }],
"fields": { "title": "title", "heading": "h1", "links": "a@href" },
"maxPagesPerCrawl": 5
}
lantfly call lantfly~web-scraper -i input.json --output-dataset > items.json
{
"url": "https://example.com/",
"title": "Example Domain",
"heading": "Example Domain",
"links": "https://www.iana.org/domains/example",
"loadedAt": "2026-11-03T08:00:01.234Z"
}
Next steps
- Start your own from the quickstart.
- Report one outcome per input with targets: Run outcome.