Lua API

Linked Repos API

Lua API for writing to repos an admin has linked to this instance. For a conceptual overview, see Linked Repos.

linked_repos table

The linked_repos table is available in all script contexts (procedures, queries, record scripts, label scripts, and job scripts).

linked_repos.list()

List every linked repo, including ones that aren't usable yet.

Returns: table — an array of tables with id, did, handle, reason, status, and scopes.

for _, repo in ipairs(linked_repos.list()) do
  log(repo.did .. " is " .. repo.status)
end

linked_repos.get(did)

Get a handle for a linked repo.

Parameters:

ParameterTypeDescription
didstringThe DID of the linked repo

Returns: a Repo handle, or nil if no repo with that DID is linked.

Raises: if the grant exists but isn't usable, either it has never been authorized (pending) or its session failed to refresh (needs_reauth). The error names which.

local repo = linked_repos.get("did:plc:abc123")
if not repo then
  return { error = "that repo isn't linked" }
end

Repo handle

Every method re-reads the grant from the database before acting. A grant deleted or flipped to needs_reauth mid-script is refused on the next call; the handle you're holding can't outlive it.

Errors raise rather than returning nil, err, matching atproto.spaces.

Fields:

FieldTypeDescription
didstringThe linked repo's DID
handlestring?The handle, when one is known
statusstringactive, pending, or needs_reauth
scopesstringThe grant's scope string

Scope checks

The typed methods below check the grant's scopes locally, before any network call. A missing scope fails immediately with a message naming the grant and the scope it needs, so you never have to work backwards from an opaque 403.

repo:call() is the exception: it can't be checked generically, so it goes straight to the PDS, which enforces the token's real scope.

Local indexing

create_record, put_record, and delete_record work the same as the Record API.

repo:call() allows the use of arbitrary XRPCs against the repo's PDS. Note that using this instead of the built-in methods (e.g. repo:call('com.atproto.repo.createRecord')) instead of repo:create_record()) bypasses HappyView's local indexing. The records will still be indexed when they come from Jetstream.

repo:create_record{...}

Create a record.

Options:

KeyTypeDescription
collectionstringRequired. The collection NSID.
recordtableRequired. The record body.
rkeystring?Optional. Generated if omitted.

Returns: table{ uri, cid }.

Requires scope: repo:<collection>?action=create

local result = repo:create_record{
  collection = "com.example.note",
  record = { text = "hello", createdAt = now() },
}
log("wrote " .. result.uri)

repo:put_record{...}

Write a record at a known rkey, or creates the record if it doesn't exist.

Options:

KeyTypeDescription
collectionstringRequired. The collection NSID.
rkeystringRequired. The record key.
recordtableRequired. The record body.
swap_cidstring?Optional. Compare-and-swap against this CID.

Returns: table{ uri, cid }.

Requires scope: both action=create and action=update — or only action=update when swap_cid is supplied.

Passing swap_cid narrows putRecord to updates only. The PDS then requires the record to already exist with that CID to guarantee no create can happen, so only update is needed. The trade-off is that you must know the current CID, which usually means reading the record first.

-- Needs create + update
repo:put_record{
  collection = "com.example.note",
  rkey = "self",
  record = { text = "hello" },
}

-- Needs only update
repo:put_record{
  collection = "com.example.note",
  rkey = "self",
  record = { text = "hello" },
  swap_cid = "bafyreib2rxk3rh6kzwq...",
}

repo:delete_record{...}

Delete a record.

Options:

KeyTypeDescription
collectionstringRequired. The collection NSID.
rkeystringRequired. The record key.

Returns: true.

Requires scope: repo:<collection>?action=delete

repo:delete_record{ collection = "com.example.note", rkey = "3k2j..." }

repo:upload_blob(bytes, mime)

Upload a blob to the linked repo's PDS.

Parameters:

ParameterTypeDescription
bytesstringThe raw bytes. Binary-safe.
mimestringThe mime type, e.g. image/png.

Returns: table — the blob reference, for embedding in a record.

Requires scope: a blob: scope matching the mime type, e.g. blob:image/* or blob:*/*.

local blob = repo:upload_blob(bytes, "image/png")
repo:create_record{
  collection = "com.example.photo",
  record = { image = blob, alt = "a cat" },
}

repo:call(nsid[, opts])

Call any XRPC method as the linked repo. This is the escape hatch for anything the typed methods don't cover, including rpc: scopes.

Parameters:

ParameterTypeDescription
nsidstringThe XRPC method to call.
optstable?params for a GET, input for a POST.

Returns: table — the decoded response.

Requires scope: not checked locally. The PDS enforces the token's actual scope.

-- GET
local out = repo:call("com.atproto.repo.listRecords", {
  params = { repo = repo.did, collection = "com.example.note", limit = 10 },
})

-- POST
repo:call("com.example.doThing", { input = { value = 42 } })

Error handling

All methods raise on failure. Use pcall if a script should continue past an error:

local ok, err = pcall(function()
  repo:create_record{ collection = "com.example.note", record = { text = "hi" } }
end)

if not ok then
  log("write failed: " .. tostring(err))
end

A grant whose session can no longer be refreshed flips to needs_reauth as a side effect of the failed call, so the dashboard reflects the problem even if your script swallows the error.

See also