Skip to main content
A processor is a fresh isolate on every run. Nothing you set in one call survives into the next by itself — so anything you want to keep goes somewhere deliberate. None of what follows needs an egress.allow entry. It is not the network.

Key/value: cursors, caches, counters

Values are opaque strings and come back byte-for-byte as you wrote them. We do not parse them, so if you store canonical JSON or something you later hash, you get exactly those bytes back — which a JSON column would not give you, because it reorders object keys and normalises numbers. Serialize it yourself. JSON.stringify of a Map, a Set, a Date or a class instance silently loses information, and the person who chose the encoding should be the one who wrote it.

Concurrent runs are normal, so use the version

Up to ten runs of your processor execute at once. A read-modify-write without a version is a lost update — two runs both read 5, both write 6, and one increment vanishes.
A conflict throws with err.code === 'version_conflict' and err.currentVersion, so you can re-read, merge and retry rather than guess. Bound your retries. Under real contention a tight loop can exhaust your attempts and your cpu_ms on conflicts alone, and the buyer is charged for that run either way. A handful of attempts with a short delay between them is enough; if you are consistently exhausting them, the contention is the problem, not the loop.

Listing

Keys and versions only, never values — a list that returned values could pull your whole store through one response. Paginate with the returned cursor until it is null. Limits: 10,000 keys, 1 KiB per key, 64 KiB per value.
Limits: 100 MiB total, 10 MiB per object, 10,000 objects. Your paths live in your own namespace — you cannot read or write another processor’s objects, and a path with .. in it is refused.

Handing a file to a buyer

That link needs no credential. Two things to understand before you put one in your output:
  • It is a bearer link. Anyone who obtains it can download until it expires — default one hour, maximum 24 hours. Treat it like a password in a URL, because that is what it is: do not print one into your logs, put one in a public page, or send one anywhere you would not send the file itself.
  • It is always served as an attachment with a forced application/octet-stream type. Your declared content type is deliberately not echoed, so an HTML or SVG file you stored cannot execute in a visitor’s browser. If you need a file rendered inline, host it yourself.

Secrets: two modes, and the difference matters

Declare a secret in your manifest, then set its value with singularity processors secrets NAME=value. We never hand a value back to you.

inject — the default, and the strong one

The value never enters your isolate. You call fetch() with no credential at all, and our egress gateway adds the header on the way out — only for the hosts you declared.
Your key is held by our egress gateway and is not readable from your code’s environment. So it cannot end up in a log line, an error message, or a response body by accident, and a dependency you pulled in cannot read it either.
An inject secret is not readable from your code. SGL.secrets.get() returns null for one, exactly as it does for a name you never declared. That is the whole point of choosing this mode.
What this does not do, because overselling it would be worse than not having it: your code can still spend the credential against the hosts you allowlisted, and if one of those hosts echoes request headers back — a debug endpoint, or a host you control — your code can read the injected value out of that response. Injection stops accidental leakage and casual exfiltration by a dependency. It is not a guarantee against code that is actively trying to see its own key.

mode: "env" — when the value has to be in your hands

Some credentials cannot be an outbound header: a key you sign something with locally, or a value an SDK insists on reading itself. For those, env exists.
This is a real downgrade, and it is opt-in per secret for that reason. Once the value is in your isolate it can be printed into your captured logs, or sent to any host in your egress allowlist, and we cannot stop either.It is also readable by every dependency you bundled, not just the code you wrote — anything running in that isolate can call SGL.secrets.get(). Treat an env secret as visible to your whole dependency tree.Use inject for anything that is only ever an outbound header. env is still better than hard-coding a secret in your source, which puts it in your stored code and in every version we retain.
singularity processors env list shows which of yours is which.
singularity processors env list shows which secrets are which, and the State tab in the dashboard shows what is in your key/value store — read-only, because it belongs to your code.

What happens to all of this when you delete a processor

It goes. State and files are removed with the processor, and its slug stays permanently reserved so no one else can take a name your buyers had saved. If you only want to stop traffic, do not delete:
Pausing refuses buyers and invoke-token callers before any payment is taken, and takes you out of the catalogue. Runs already in flight finish. You can still invoke it yourself with your wallet, so you can verify a fix before resuming — and those owner runs still cost you compute, because a run costs us the same whoever triggered it. Unlisting is not the same thing: an unlisted processor is out of the catalogue but its endpoint keeps answering anyone who has the URL or an invoke token. Those calls earn you nothing and still draw compute from your credit balance, which is the part that surprises people.