Skip to main content

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​

FieldTypeDefaultLimits
messagestringHello from Lantfly
countinteger31–1000

Default options: 256 MB memory, 120 seconds timeout.

Code​

src/main.ts
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:

  1. Actor.main wraps the work: it initializes the SDK, and at the end flushes the items and exits with code 0.
  2. Actor.getInput reads the INPUT record. The platform already filled in the schema defaults; the code defaults only matter for local runs.
  3. Actor.pushData buffers items and sends them in batches.
  4. Actor.setValue('OUTPUT', …) writes its own summary. Because the actor never reports targets, the SDK keeps this OUTPUT as it is.

Run it​

Terminal
lantfly call lantfly~hello --input '{"message": "Hi", "count": 2}' --output-dataset

Output items:

Dataset 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​

FieldTypeDefaultMeaning
startUrlsarray of { "url": … }requiredPages to start from.
linkSelectorstringCSS selector of links to follow, for example a.next.
globsarray of strings[]Only follow links whose URL matches one of these globs. * matches anything but /, ** matches anything.
fieldsobject{"title": "title"}Field name → CSS selector. End the selector with @attr to read an attribute (a.next@href, meta[name=description]@content).
maxPagesPerCrawlinteger50Stop after this many pages.
maxConcurrencyinteger5Pages fetched at the same time (at most 50).
proxyConfigurationobject{"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.
  • @href and @src values are turned into absolute URLs.

Code​

src/main.ts
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:

  1. 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.
  2. Proxy from the input. createProxyConfiguration takes the input's proxyConfiguration object and returns null when useLantflyProxy is false. Crawlee gets the Lantfly proxy through newUrlFunction, so each Crawlee session keeps its own sticky exit IP.
  3. Crawlee's request queue. Links are queued with enqueueLinks; the queue lives in the container.
  4. Failures. A page that fails after Crawlee's retries is logged as a warning and the crawl goes on.
  5. 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​

input.json
{
"startUrls": [{ "url": "https://example.com" }],
"fields": { "title": "title", "heading": "h1", "links": "a@href" },
"maxPagesPerCrawl": 5
}
Terminal
lantfly call lantfly~web-scraper -i input.json --output-dataset > items.json
Dataset item
{
"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​