Apify Desk API
Everything the web app does over the model is one HTTP call. Base URL https://api.skillsafe.ai/v1/app-api. Every request carries Authorization: Bearer <token> and every response uses the same envelope.
The response envelope
Success is {"ok": true, "data": {...}}. Failure is {"ok": false, "error": {"code": "...", "message": "...", "details": {...}}}. Always branch on ok, never on the HTTP status alone.
| Code | HTTP | What it means here |
|---|---|---|
UNAUTHORIZED | 401 | Missing, malformed or expired token. Mint a new one on the token page. |
PAYMENT_REQUIRED | 402 | Balance below min_credits for this lane. Call /estimate first and top up. |
VALIDATION_ERROR | 400 | The input object is the wrong shape — usually a missing source or an unknown task. |
RATE_LIMITED | 429 | Back off and retry. Do not tight-loop. |
NOT_FOUND | 404 | Wrong job id, or a job that belongs to another subject. |
INTERNAL | 500 | Retry once with the same idempotency key. |
The task field comes first
Apify Desk is one app with four lanes over one work object — an Apify Actor project. Every request must set task; it selects the lane, the prompt section, the output body shape and the price. If task is missing the model picks the closest lane and reports lane_inferred: true — usable, but never what you want from a script.
task | What that lane returns | body keys |
|---|---|---|
actorize | The gap table between the script and an Actor, the file plan, the ordered migration steps, and the rewritten entrypoint as the artifact. | gap_table, file_plan, migration_steps |
input | Every input field with its type, editor, requiredness and prefill, grouped into sections, plus a complete INPUT_SCHEMA.json as the artifact. | fields, sections, validation_notes |
output | The dataset fields with types and provenance, the table views, the key-value records, plus a complete dataset_schema.json as the artifact. | dataset_fields, views, kv_records, output_schema |
client | The apify-client call plan, the input mapping, result and error handling, plus a runnable client module as the artifact. | call_plan, input_mapping, result_handling, error_handling |
Input fields
Taken from readForm() in app.js — this is exactly what the web app sends.
| Field | Type | Required | Notes |
|---|---|---|---|
task | string | yes | One of actorize, input, output, client. |
source | string | yes | The pasted Actor project. Separate files with a // file: path line (or # file: path for Python and Dockerfiles). Clipped from the middle at 52,000 characters, both ends kept. |
notes | string | no | Free text about what the Actor is for. The client lane leans on it hardest — the caller's language, whether it waits or polls, what it does with the dataset — because none of that is in the Actor. |
runtime | string | no | node, python, mixed or unknown. Omit it and the model reads the runtime off the source itself. |
prescan | object | no | What the browser reader found: documents, datasetFields, input_properties, checks and numbered flags. Omit it and the model simply has fewer facts — but coverage_check then comes back empty, because there are no flags to reconcile. |
clip_note | string | no | Send it when you clipped source yourself, so the model writes around the gap rather than inventing across it. |
Step 1 — get a token
Open the token page, reveal your token and copy the shell export. It is the same token the web app holds in this browser, so a script and the page share one identity, one balance and one history. Keep it out of source control — export it as APIFY_DESK_TOKEN and read it from there, the way every sample below does.
For an unattended script with no browser, mint a guest token instead. This is the only endpoint that names the app: every other call identifies the app from the token itself, so there is no per-slug path to call.
curl -sS -X POST "https://api.skillsafe.ai/v1/app-api/guest" \
-H "Content-Type: application/json" \
-d '{"slug": "apify-desk"}'import json, urllib.request
req = urllib.request.Request("https://api.skillsafe.ai/v1/app-api/guest",
data=json.dumps({"slug": "apify-desk"}).encode(),
headers={"Content-Type": "application/json"})
token = json.load(urllib.request.urlopen(req))["data"]["token"]
print(token)const res = await fetch("https://api.skillsafe.ai/v1/app-api/guest", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ slug: "apify-desk" })
});
const { data } = await res.json();
const TOKEN = data.token;body := strings.NewReader(`{"slug": "apify-desk"}`)
res, _ := http.Post("https://api.skillsafe.ai/v1/app-api/guest", "application/json", body)
defer res.Body.Close()
// data.token is your bearer tokenvar req = HttpRequest.newBuilder(URI.create("https://api.skillsafe.ai/v1/app-api/guest"))
.header("Content-Type", "application/json")
.POST(HttpRequest.BodyPublishers.ofString("{\"slug\": \"apify-desk\"}"))
.build();
// read data.token out of the response bodyuri = URI("https://api.skillsafe.ai/v1/app-api/guest")
res = Net::HTTP.post(uri, { slug: "apify-desk" }.to_json,
"Content-Type" => "application/json")
token = JSON.parse(res.body)["data"]["token"]<?php
$ch = curl_init("https://api.skillsafe.ai/v1/app-api/guest");
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode(["slug" => "apify-desk"]));
curl_setopt($ch, CURLOPT_HTTPHEADER, ["Content-Type: application/json"]);
$token = json_decode(curl_exec($ch), true)["data"]["token"];var payload = new StringContent("{\"slug\": \"apify-desk\"}",
Encoding.UTF8, "application/json");
var res = await http.PostAsync("https://api.skillsafe.ai/v1/app-api/guest", payload);
// data.token is your bearer tokenA guest subject has its own wallet and its own history. Records written under one guest token are invisible to the next one, so keep the token if you want the runs back.
Step 2 — confirm the session and the balance
GET /me is free. It tells you whether the token is a personal or a guest subject and how many credits it can spend.
curl -sS "https://api.skillsafe.ai/v1/app-api/me" \
-H "Authorization: Bearer $APIFY_DESK_TOKEN"import json, os, urllib.request
TOKEN = os.environ.get("APIFY_DESK_TOKEN", "YOUR_TOKEN")
req = urllib.request.Request("https://api.skillsafe.ai/v1/app-api/me",
headers={"Authorization": "Bearer " + TOKEN})
print(json.load(urllib.request.urlopen(req)))const TOKEN = "YOUR_TOKEN";
const res = await fetch("https://api.skillsafe.ai/v1/app-api/me", {
headers: { Authorization: `Bearer ${TOKEN}` }
});
const body = await res.json();
if (!body.ok) throw new Error(body.error.code + ": " + body.error.message);
console.log(body.data);package main
import (
"fmt"
"io"
"net/http"
"os"
)
func main() {
req, _ := http.NewRequest("GET", "https://api.skillsafe.ai/v1/app-api/me", nil)
req.Header.Set("Authorization", "Bearer "+os.Getenv("APIFY_DESK_TOKEN"))
res, err := http.DefaultClient.Do(req)
if err != nil {
panic(err)
}
defer res.Body.Close()
b, _ := io.ReadAll(res.Body)
fmt.Println(string(b))
}import java.net.URI;
import java.net.http.*;
var token = System.getenv("APIFY_DESK_TOKEN");
var client = HttpClient.newHttpClient();
var req = HttpRequest.newBuilder(URI.create("https://api.skillsafe.ai/v1/app-api/me"))
.header("Authorization", "Bearer " + token)
.GET().build();
System.out.println(client.send(req, HttpResponse.BodyHandlers.ofString()).body());require "net/http"
require "json"
require "uri"
uri = URI("https://api.skillsafe.ai/v1/app-api/me")
req = Net::HTTP::Get.new(uri)
req["Authorization"] = "Bearer #{ENV.fetch('APIFY_DESK_TOKEN', 'YOUR_TOKEN')}"
res = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |h| h.request(req) }
puts JSON.parse(res.body)<?php
$token = getenv("APIFY_DESK_TOKEN") ?: "YOUR_TOKEN";
$ch = curl_init("https://api.skillsafe.ai/v1/app-api/me");
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, ["Authorization: Bearer $token"]);
$body = json_decode(curl_exec($ch), true);
var_dump($body["data"]);using System.Net.Http;
using System.Net.Http.Headers;
var token = Environment.GetEnvironmentVariable("APIFY_DESK_TOKEN") ?? "YOUR_TOKEN";
using var http = new HttpClient();
http.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token);
Console.WriteLine(await http.GetStringAsync("https://api.skillsafe.ai/v1/app-api/me"));Step 3 — price the run before you make it
POST /estimate is free, makes no job and charges nothing. It returns model, model_alias, markup_bps, hold_credits and min_credits for this exact input. The hold differs per lane, so estimate the lane you are about to run — never reuse another lane's number.
curl -sS -X POST "https://api.skillsafe.ai/v1/app-api/estimate" \
-H "Authorization: Bearer $APIFY_DESK_TOKEN" \
-H "Content-Type: application/json" \
-d @input.jsonimport json, os, urllib.request
TOKEN = os.environ.get("APIFY_DESK_TOKEN", "YOUR_TOKEN")
payload = {
"task": "actorize", # actorize | input | output | client
"source": actor_project_text, # the pasted project
"notes": "runs nightly, Store-bound",
"runtime": "node",
}
req = urllib.request.Request("https://api.skillsafe.ai/v1/app-api/estimate",
data=json.dumps(payload).encode(),
headers={"Authorization": "Bearer " + TOKEN,
"Content-Type": "application/json"})
print(json.load(urllib.request.urlopen(req))["data"])const TOKEN = "YOUR_TOKEN";
const payload = {
task: "actorize", // actorize | input | output | client
source: actorProjectText, // the pasted project, files split by "// file: name"
notes: "runs nightly, Store-bound",
runtime: "node" // node | python | mixed | unknown
};
const res = await fetch("https://api.skillsafe.ai/v1/app-api/estimate", {
method: "POST",
headers: { Authorization: `Bearer ${TOKEN}`, "Content-Type": "application/json" },
body: JSON.stringify(payload)
});
const body = await res.json();
if (!body.ok) throw new Error(body.error.code);
console.log(body.data);package main
import (
"bytes"
"encoding/json"
"fmt"
"io"
"net/http"
"os"
)
func main() {
payload := map[string]any{"task": "actorize", "source": actorSource}
body, _ := json.Marshal(payload)
req, _ := http.NewRequest("POST", "https://api.skillsafe.ai/v1/app-api/estimate", bytes.NewReader(body))
req.Header.Set("Authorization", "Bearer "+os.Getenv("APIFY_DESK_TOKEN"))
req.Header.Set("Content-Type", "application/json")
res, err := http.DefaultClient.Do(req)
if err != nil {
panic(err)
}
defer res.Body.Close()
b, _ := io.ReadAll(res.Body)
fmt.Println(string(b))
}import java.net.URI;
import java.net.http.*;
var token = System.getenv("APIFY_DESK_TOKEN");
var payload = """
{"task": "actorize", "source": "...your Actor project..."}
""";
var req = HttpRequest.newBuilder(URI.create("https://api.skillsafe.ai/v1/app-api/estimate"))
.header("Authorization", "Bearer " + token)
.header("Content-Type", "application/json")
.POST(HttpRequest.BodyPublishers.ofString(payload)).build();
System.out.println(HttpClient.newHttpClient()
.send(req, HttpResponse.BodyHandlers.ofString()).body());require "net/http"
require "json"
require "uri"
uri = URI("https://api.skillsafe.ai/v1/app-api/estimate")
req = Net::HTTP::Post.new(uri)
req["Authorization"] = "Bearer #{ENV.fetch('APIFY_DESK_TOKEN', 'YOUR_TOKEN')}"
req["Content-Type"] = "application/json"
req.body = { task: "actorize", source: actor_source }.to_json
res = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |h| h.request(req) }
puts JSON.parse(res.body)["data"]<?php
$token = getenv("APIFY_DESK_TOKEN") ?: "YOUR_TOKEN";
$payload = json_encode(["task" => "actorize", "source" => $actorSource]);
$ch = curl_init("https://api.skillsafe.ai/v1/app-api/estimate");
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, $payload);
curl_setopt($ch, CURLOPT_HTTPHEADER, ["Authorization: Bearer $token",
"Content-Type: application/json"]);
$body = json_decode(curl_exec($ch), true);
var_dump($body["data"]);using System.Net.Http;
using System.Net.Http.Headers;
using System.Text;
var token = Environment.GetEnvironmentVariable("APIFY_DESK_TOKEN") ?? "YOUR_TOKEN";
using var http = new HttpClient();
http.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token);
var payload = new StringContent(json, Encoding.UTF8, "application/json");
var res = await http.PostAsync("https://api.skillsafe.ai/v1/app-api/estimate", payload);
Console.WriteLine(await res.Content.ReadAsStringAsync());Step 4 — run it
POST /run is metered. It returns {"job_id": "..."} immediately; poll GET /jobs/{job_id} until status is succeeded or failed.
Pass Idempotency-Key on every run. The web app derives it from (slug, task, hash(task+source+notes), attempt), so two lanes over the same project are two distinct runs and a retried network call can never bill twice.
curl -sS -X POST "https://api.skillsafe.ai/v1/app-api/run" \
-H "Authorization: Bearer $APIFY_DESK_TOKEN" \
-H "Content-Type: application/json" \
-d @input.jsonimport json, os, urllib.request
TOKEN = os.environ.get("APIFY_DESK_TOKEN", "YOUR_TOKEN")
payload = {
"task": "actorize", # actorize | input | output | client
"source": actor_project_text, # the pasted project
"notes": "runs nightly, Store-bound",
"runtime": "node",
}
req = urllib.request.Request("https://api.skillsafe.ai/v1/app-api/run",
data=json.dumps(payload).encode(),
headers={"Authorization": "Bearer " + TOKEN,
"Content-Type": "application/json"})
print(json.load(urllib.request.urlopen(req))["data"])const TOKEN = "YOUR_TOKEN";
const payload = {
task: "actorize", // actorize | input | output | client
source: actorProjectText, // the pasted project, files split by "// file: name"
notes: "runs nightly, Store-bound",
runtime: "node" // node | python | mixed | unknown
};
const res = await fetch("https://api.skillsafe.ai/v1/app-api/run", {
method: "POST",
headers: { Authorization: `Bearer ${TOKEN}`, "Content-Type": "application/json" },
body: JSON.stringify(payload)
});
const body = await res.json();
if (!body.ok) throw new Error(body.error.code);
console.log(body.data);package main
import (
"bytes"
"encoding/json"
"fmt"
"io"
"net/http"
"os"
)
func main() {
payload := map[string]any{"task": "actorize", "source": actorSource}
body, _ := json.Marshal(payload)
req, _ := http.NewRequest("POST", "https://api.skillsafe.ai/v1/app-api/run", bytes.NewReader(body))
req.Header.Set("Authorization", "Bearer "+os.Getenv("APIFY_DESK_TOKEN"))
req.Header.Set("Content-Type", "application/json")
res, err := http.DefaultClient.Do(req)
if err != nil {
panic(err)
}
defer res.Body.Close()
b, _ := io.ReadAll(res.Body)
fmt.Println(string(b))
}import java.net.URI;
import java.net.http.*;
var token = System.getenv("APIFY_DESK_TOKEN");
var payload = """
{"task": "actorize", "source": "...your Actor project..."}
""";
var req = HttpRequest.newBuilder(URI.create("https://api.skillsafe.ai/v1/app-api/run"))
.header("Authorization", "Bearer " + token)
.header("Content-Type", "application/json")
.POST(HttpRequest.BodyPublishers.ofString(payload)).build();
System.out.println(HttpClient.newHttpClient()
.send(req, HttpResponse.BodyHandlers.ofString()).body());require "net/http"
require "json"
require "uri"
uri = URI("https://api.skillsafe.ai/v1/app-api/run")
req = Net::HTTP::Post.new(uri)
req["Authorization"] = "Bearer #{ENV.fetch('APIFY_DESK_TOKEN', 'YOUR_TOKEN')}"
req["Content-Type"] = "application/json"
req.body = { task: "actorize", source: actor_source }.to_json
res = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |h| h.request(req) }
puts JSON.parse(res.body)["data"]<?php
$token = getenv("APIFY_DESK_TOKEN") ?: "YOUR_TOKEN";
$payload = json_encode(["task" => "actorize", "source" => $actorSource]);
$ch = curl_init("https://api.skillsafe.ai/v1/app-api/run");
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, $payload);
curl_setopt($ch, CURLOPT_HTTPHEADER, ["Authorization: Bearer $token",
"Content-Type: application/json"]);
$body = json_decode(curl_exec($ch), true);
var_dump($body["data"]);using System.Net.Http;
using System.Net.Http.Headers;
using System.Text;
var token = Environment.GetEnvironmentVariable("APIFY_DESK_TOKEN") ?? "YOUR_TOKEN";
using var http = new HttpClient();
http.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token);
var payload = new StringContent(json, Encoding.UTF8, "application/json");
var res = await http.PostAsync("https://api.skillsafe.ai/v1/app-api/run", payload);
Console.WriteLine(await res.Content.ReadAsStringAsync());Step 5 — poll the job
curl -sS "https://api.skillsafe.ai/v1/app-api/jobs/JOB_ID" \
-H "Authorization: Bearer $APIFY_DESK_TOKEN"import json, os, urllib.request
TOKEN = os.environ.get("APIFY_DESK_TOKEN", "YOUR_TOKEN")
req = urllib.request.Request("https://api.skillsafe.ai/v1/app-api/jobs/JOB_ID",
headers={"Authorization": "Bearer " + TOKEN})
print(json.load(urllib.request.urlopen(req)))const TOKEN = "YOUR_TOKEN";
const res = await fetch("https://api.skillsafe.ai/v1/app-api/jobs/JOB_ID", {
headers: { Authorization: `Bearer ${TOKEN}` }
});
const body = await res.json();
if (!body.ok) throw new Error(body.error.code + ": " + body.error.message);
console.log(body.data);package main
import (
"fmt"
"io"
"net/http"
"os"
)
func main() {
req, _ := http.NewRequest("GET", "https://api.skillsafe.ai/v1/app-api/jobs/JOB_ID", nil)
req.Header.Set("Authorization", "Bearer "+os.Getenv("APIFY_DESK_TOKEN"))
res, err := http.DefaultClient.Do(req)
if err != nil {
panic(err)
}
defer res.Body.Close()
b, _ := io.ReadAll(res.Body)
fmt.Println(string(b))
}import java.net.URI;
import java.net.http.*;
var token = System.getenv("APIFY_DESK_TOKEN");
var client = HttpClient.newHttpClient();
var req = HttpRequest.newBuilder(URI.create("https://api.skillsafe.ai/v1/app-api/jobs/JOB_ID"))
.header("Authorization", "Bearer " + token)
.GET().build();
System.out.println(client.send(req, HttpResponse.BodyHandlers.ofString()).body());require "net/http"
require "json"
require "uri"
uri = URI("https://api.skillsafe.ai/v1/app-api/jobs/JOB_ID")
req = Net::HTTP::Get.new(uri)
req["Authorization"] = "Bearer #{ENV.fetch('APIFY_DESK_TOKEN', 'YOUR_TOKEN')}"
res = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |h| h.request(req) }
puts JSON.parse(res.body)<?php
$token = getenv("APIFY_DESK_TOKEN") ?: "YOUR_TOKEN";
$ch = curl_init("https://api.skillsafe.ai/v1/app-api/jobs/JOB_ID");
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, ["Authorization: Bearer $token"]);
$body = json_decode(curl_exec($ch), true);
var_dump($body["data"]);using System.Net.Http;
using System.Net.Http.Headers;
var token = Environment.GetEnvironmentVariable("APIFY_DESK_TOKEN") ?? "YOUR_TOKEN";
using var http = new HttpClient();
http.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token);
Console.WriteLine(await http.GetStringAsync("https://api.skillsafe.ai/v1/app-api/jobs/JOB_ID"));Step 6 — or stream it
POST /run-stream is the same call with an SSE response: delta events carry output as it is produced, and a final done event carries the whole reply plus charged_credits and truncated. The web app uses this one, and advances its progress card on the section headings arriving in the stream.
curl -sSN -X POST "https://api.skillsafe.ai/v1/app-api/run-stream" \
-H "Authorization: Bearer $APIFY_DESK_TOKEN" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: apify-desk:output:$(date +%s)" \
-d @input.jsonimport json, os, urllib.request
TOKEN = os.environ.get("APIFY_DESK_TOKEN", "YOUR_TOKEN")
req = urllib.request.Request("https://api.skillsafe.ai/v1/app-api/run-stream",
data=json.dumps(payload).encode(),
headers={"Authorization": "Bearer " + TOKEN,
"Content-Type": "application/json",
"Idempotency-Key": "apify-desk:output:1"})
for raw in urllib.request.urlopen(req):
line = raw.decode().strip()
if line.startswith("data: "):
print(json.loads(line[6:]))const res = await fetch("https://api.skillsafe.ai/v1/app-api/run-stream", {
method: "POST",
headers: {
Authorization: `Bearer ${TOKEN}`,
"Content-Type": "application/json",
"Idempotency-Key": "apify-desk:output:1"
},
body: JSON.stringify(payload)
});
const reader = res.body.getReader();
const dec = new TextDecoder();
for (;;) {
const { done, value } = await reader.read();
if (done) break;
process.stdout.write(dec.decode(value));
}// Post to https://api.skillsafe.ai/v1/app-api/run-stream and read the body line by line;
// each "data: {...}" line is one SSE event.// POST to https://api.skillsafe.ai/v1/app-api/run-stream with BodyHandlers.ofLines()
// and parse each "data: " prefixed line.# POST to https://api.skillsafe.ai/v1/app-api/run-stream and read the response body in chunks;
# each "data: {...}" line is one SSE event.<?php
// POST to https://api.skillsafe.ai/v1/app-api/run-stream with CURLOPT_WRITEFUNCTION
// and parse each "data: " prefixed line as it arrives.// POST to https://api.skillsafe.ai/v1/app-api/run-stream, then read the response stream
// with a StreamReader and parse each "data: " line.The output contract
Every lane returns the same outer envelope and differs only inside body. This is what normalize() in app.js enforces, so anything below is safe to rely on.
{
"lane": "output",
"lane_inferred": false,
"title": "Example Shop Scraper - dataset and key-value output",
"posture": "fix-first",
"verdict": "The Actor pushes seven well-shaped fields but declares no dataset schema.",
"runtime": "node",
"actor_name": "example-shop-scraper",
"summary": "...",
"assumptions": ["..."],
"open_questions": ["..."],
"findings": [
{"id": "AD-001", "title": "...", "severity": "medium", "area": "output-schema",
"file": "src/main.js", "line": 16, "evidence": "...", "why": "...",
"fix": "...", "fix_code": "..."}
],
"coverage_check": [
{"flag_id": "S11", "status": "confirmed", "finding_id": "AD-001", "note": ""}
],
"artifact": {"kind": "json", "filename": ".actor/dataset_schema.json", "content": "{ ... }"},
"next_lane": {"lane": "client", "reason": "..."},
"body": { "dataset_fields": [], "views": [], "kv_records": [] }
}
Unknown values are coerced rather than rejected: an unrecognised posture becomes fix-first, an unrecognised runtime becomes unknown, an unrecognised severity becomes medium, and an artifact whose content is empty is downgraded to kind: "none". A reply that does not name a known lane is the one thing that is rejected outright.
One worked example per lane
task: "actorize" — Actorize the script
{"task": "actorize", "source": "// file: scrape.js\nconst axios = require('axios');\n// ... your script ...", "notes": ""}
Returns posture: "not-actor-yet" for a plain script, a gap row per missing piece, and artifact.kind: "javascript" holding src/main.js rewritten around Actor.init().
task: "input" — Author the input schema
{"task": "input", "source": "// file: src/main.js\nimport { Actor } from 'apify';\n// ...", "notes": "Store-bound; the form must be usable by a non-developer"}
Returns one body.fields entry per option the code really reads, and artifact.content holding a complete .actor/input_schema.json.
task: "output" — Generate the output schemas
{"task": "output", "source": "// file: src/main.js\n// ... await Actor.pushData(item) ...", "notes": "priceCents is minor units"}
Returns one body.dataset_fields entry per field the code pushes — starting from the reader's list — and a complete .actor/dataset_schema.json as the artifact.
task: "client" — Wire the client call
{"task": "client", "source": "// file: src/main.js ... // file: .actor/input_schema.json ...", "notes": "Node caller, runs nightly, writes to Postgres"}
Returns the call plan and a mapping of every required input property, with artifact.kind: "javascript" holding a runnable run-actor.js.
Reconciling the reader
If you send prescan, every flag id in prescan.flags comes back as exactly one coverage_check entry. A flag with no entry is the model ignoring a deterministic fact, and the web app renders that as not accounted for rather than hiding it. Scripts should assert the same thing.