# Artefaktum docs > Core concepts behind Artefaktum, from artifacts and versions to search, lineage, and how bytes move Artefaktum is a multi-tenant artifact store for AI agents and automated workflows that share no filesystem. An agent pushes a file with a descriptor; a different agent, on a different host, hours or days later, finds it by what it is rather than by an ID, and checks whether it is still current before it relies on it. ## Artifact An artifact is the stable identity for a file. Your code sets its title, description, tags, and custom metadata when it pushes; the server fills in content type, size in bytes, and sha256 once the bytes land. Status moves through `pending_upload`, `processing`, `ready`, `failed`, `deleting`, `deleted`. `ready` means the object is verified in storage, not that it is searchable by meaning yet: semantic readiness is tracked separately as `semantic_ready`, so an embedding-provider outage never hides a new artifact from exact or text search, only from semantic. ## Versions An artifact can have more than one version, and bytes are immutable: new content never overwrites a version, it opens one with `artifact_create_version` (`create_version` in the SDKs), and the server records a `supersedes` edge from the new version to the one before it. Search results and `artifact_get` return the latest version's metadata, content type, size, and sha256, by default. Older versions stay addressable by ID. ## external_key `external_key` is a stable name your own code chooses, not one the server hands back, so re-running the same workflow, or a different workflow entirely, lands on the same artifact instead of creating a duplicate. `resolve` is built around it: pass an `external_key` and a `max_age_seconds`, and it returns `hit` when a fresh-enough artifact already exists, `create` with a signed upload URL when you are the caller that should produce it, or `pending` when another caller is already producing it. That is what lets several agents racing for the same expensive upstream response make it exactly once. ## Search modes `artifact_search` supports four modes: `exact` (structured filters only, no query text needed), `text` (full-text over title, description, and tags), `semantic` (over the artifact descriptor), and `hybrid`, which fuses the text and semantic rank lists with Reciprocal Rank Fusion so no score calibration is needed. Scores are not comparable across modes. Any mode can be narrowed with filters on tags, content type, and dates. ## Relations and lineage Artifacts link to each other with five relation types: `derived_from`, `supersedes`, `attachment_of`, `generated_by`, and `related_to`. Tag an upload or download with a `run_id` and the server infers `derived_from` edges automatically, pinned to the exact version that was read; an explicit relation always replaces an inferred one. Two trust signals ride along with search results and `artifact_get`: `superseded`, meaning another artifact has a `supersedes` edge pointing here, or a newer version of this same artifact exists; and `stale_upstream`, meaning something this artifact was derived from has since moved on: a new version, superseded, expired, or deleted. Superseded artifacts leave default search results; pass `exclude_superseded: false` to see the history. ## Projects and tenants Every account or organisation is one tenant. Projects live inside a tenant, and every operation is scoped to a project. API keys carry scopes and can be limited to a single project or left tenant-wide, plus an optional expiry. Organisations share one plan and mint their own keys from the console. ## How bytes move Uploading is two calls, never one: reserve an upload to get a signed PUT URL and headers, send the bytes straight to object storage, then call complete. Downloading mirrors that: ask for a download and get back a short-lived signed URL, then fetch it yourself. Neither the REST API nor MCP responses ever carry file bytes, only metadata and URLs. The SDKs hash the file locally on push and verify the sha256 digest on pull, raising an integrity error on a mismatch. ## Choose an integration | Integration | Pick it when | Docs | | --- | --- | --- | | MCP | Your agent runs inside a host that speaks MCP, such as Claude Code or Cursor, and you want push, search, and pull as tool calls with nothing to install. | [/docs/mcp/](/docs/mcp/) | | Python SDK | You are writing Python and want typed methods with retries and sha256 verification built in. | [/docs/python/](/docs/python/) | | TypeScript SDK | You are writing Node, or a script for Bun, Deno, or Cloudflare Workers, and want a zero-dependency typed client. | [/docs/typescript/](/docs/typescript/) | | CLI | You are scripting, or baking Artefaktum into a sandbox image or a CI step, and want a plain command. | [/docs/cli/](/docs/cli/) | | n8n | You are building an n8n workflow and want a node with a credential, not a raw HTTP request. | [/docs/n8n/](/docs/n8n/) | | REST | You are on Make, Zapier, anything with an HTTP step, or writing your own client. | [/docs/rest/](/docs/rest/) | ## For agents The whole set is also available in one fetch: [/llms.txt](/llms.txt) is an index of every page, and [/llms-full.txt](/llms-full.txt) concatenates all of them. Every page on this site also exists as plain Markdown at `/docs/.md`. Source: https://artefaktum.dev/docs/ --- # Quick start > Get an API key, then push, search, and pull an artifact in under five minutes ## Get an API key Sign in with Google or GitHub at [artefaktum.dev/console](/console/) and create a key. Set it as `ARTEFAKTUM_API_KEY` in your shell, sandbox image, or CI secret. ## Fastest path: the CLI ```sh pip install "artefaktum[cli]" export ARTEFAKTUM_API_KEY=ak_... artefaktum push report.pdf --title "Q3 churn" --tag churn artefaktum search "q3 churn" artefaktum pull 0199198a-8f21-... out/ ``` `afk` is the short alias for `artefaktum`. `pull` verifies the sha256 digest by default and writes into `out/`, creating it if needed. ## Python ```sh pip install artefaktum ``` ```python from artefaktum import Artefaktum client = Artefaktum() # ARTEFAKTUM_API_KEY, project "default" a = client.artifacts.push("report.pdf", title="Q3 churn", tags=["churn"]) hits = client.artifacts.search("q3 churn") client.artifacts.pull(hits.items[0].artifact.id, "out/") ``` ## TypeScript ```sh npm install artefaktum ``` ```ts import { Artefaktum } from "artefaktum"; const client = new Artefaktum(); // ARTEFAKTUM_API_KEY, project "default" const artifact = await client.artifacts.push("report.pdf", { title: "Q3 churn analysis", tags: ["churn", "q3"], external_key: "reports/q3-churn", }); const page = await client.artifacts.search("q3 churn"); await client.artifacts.pull(page.items[0].artifact.id, "out/"); // sha256 verified ``` ## REST ```sh curl -s -X POST https://api.artefaktum.dev/v1/artifacts/search \ -H "Authorization: Bearer $ARTEFAKTUM_API_KEY" -H "Content-Type: application/json" \ -d '{"project_id": "…", "query": "latest enriched competitor list", "mode": "hybrid"}' ``` Pushing over raw REST is a signed-upload flow: `POST /v1/artifacts/uploads` reserves the artifact and returns a PUT URL, you upload the bytes directly to that URL, then you call the completion endpoint. Downloading is a signed GET URL from `GET /v1/artifacts/{artifact_id}/download`. See [/docs/rest/](/docs/rest/) for the full request and response shapes. ## Connect an MCP host ```sh claude mcp add --transport http artefaktum https://api.artefaktum.dev/mcp ``` Cursor, or any host that reads an `mcp.json`: ```json { "mcpServers": { "artefaktum": { "url": "https://api.artefaktum.dev/mcp" } } } ``` Sign in with Google or GitHub when the host asks; there is no key to paste. The host then has `project_list`, `artifact_search`, `artifact_get`, `artifact_list`, `artifact_resolve`, `artifact_create_upload`, `artifact_complete_upload`, `artifact_create_version`, `artifact_get_download_url`, `artifact_add_relation`, `artifact_update_metadata`, and `artifact_delete` as tools. File bytes never enter a tool response; the host fetches a short-lived URL instead. ## What to do next - Read [the docs index](/docs/) for the concepts behind artifacts, versions, search modes, and lineage. - Pick your integration in detail: [/docs/mcp/](/docs/mcp/), [/docs/python/](/docs/python/), [/docs/typescript/](/docs/typescript/), [/docs/cli/](/docs/cli/), [/docs/n8n/](/docs/n8n/), or [/docs/rest/](/docs/rest/). - Use `resolve` instead of `push` when several workflows might fetch the same expensive result; see the external_key section on [the docs index](/docs/). - Check plan limits and what happens over them at [/pricing/](/pricing/). Source: https://artefaktum.dev/docs/quickstart/ --- # MCP server > Connect an MCP host to Artefaktum's hosted server and use its tools to find, upload and trust artifacts. ## Connecting Artefaktum runs one hosted MCP server over Streamable HTTP: ``` https://api.artefaktum.dev/mcp ``` No stdio entry point; a stdio-only host needs a bridge such as `mcp-remote`. Claude Code: ```sh claude mcp add --transport http artefaktum https://api.artefaktum.dev/mcp ``` Cursor, Windsurf, Cline, or any host that reads an `mcp.json`: ```json { "mcpServers": { "artefaktum": { "url": "https://api.artefaktum.dev/mcp" } } } ``` ### What happens on first use The host opens a browser tab for an OAuth 2.1 sign-in handled by Clerk: sign in with Google or GitHub, approve the connection once, and the host gets an access token it attaches to every call after that. There is no key to paste. Sign-in provisions a tenant the first time it happens, keyed to your personal account or Clerk organisation, with a default project already in place. ## Authentication The endpoint accepts two bearer credentials, checked by the same resolver chain as REST: - **OAuth 2.1 access token**: issued by Clerk after the browser sign-in above. The default for hosts with an OAuth-capable MCP client. - **API key**: the same key used against the REST API, created in the [console](https://artefaktum.dev/console/), sent as `Authorization: Bearer `. Use it for hosts that can't complete a browser flow, or for unattended runs: pass `--header` on `claude mcp add`, or add a header next to `url` in `mcp.json` if the host allows one. Two OAuth caveats: Clerk's tokens carry no audience (`aud`) claim, so audience checking is relaxed rather than strict; and Clerk doesn't know Artefaktum's scope names, so every OAuth caller gets a fixed grant of read, write and search, never delete or admin. An API key carries exactly the scopes it was minted with. Neither matters for normal use, only for auditing what an OAuth agent can reach. ## Tools Every tool is scoped to a project. Call `project_list` first if you don't have a `project_id` yet. This matters most after OAuth sign-in, which binds you to a tenant, not a project. List-shaped tools cap at 25 items and truncate long text; file bytes never enter a tool response. | Tool | Purpose | |---|---| | `artifact_search` | Find artifacts by meaning, text, or structured filters. | | `artifact_get` | Read one artifact's metadata, lifecycle state, trust signals and relations. | | `artifact_get_download_url` | Get a short-lived signed URL for an artifact's bytes. | | `project_list` | List the projects this credential can use. | | `artifact_list` | List artifacts in a project, with filters and a cursor. | | `artifact_create_upload` | Reserve a new artifact and get a signed upload URL. | | `artifact_complete_upload` | Confirm the bytes were uploaded so the server can verify and ready it. | | `artifact_resolve` | Reuse a fresh artifact by external key, or claim the right to create it. | | `artifact_update_metadata` | Change an artifact's title, description, tags, metadata or TTL. | | `artifact_create_version` | Start a new immutable version of an existing artifact. | | `artifact_add_relation` | State a lineage relation between two artifacts. | | `artifact_delete` | Request permanent deletion of an artifact. | ### Finding artifacts **`artifact_search`** matches descriptors (title, description, tags, metadata), never file contents. `mode` is `exact` (filters only), `text` (full-text), `semantic` (pgvector) or `hybrid` (fuses both; the default). Scores aren't comparable across modes. Superseded artifacts are hidden unless asked for. | Parameter | Type | Default | |---|---|---| | `project_id` | UUID | required | | `query` | string | `""` | | `mode` | string | `"hybrid"` | | `tags_all` | string[] | none | | `content_types` | string[] | none | | `exclude_superseded` | boolean | `true` | | `limit` | integer | `20` (capped at 25) | ```json { "project_id": "3f5c...", "query": "q3 churn analysis", "mode": "hybrid", "tags_all": ["churn"], "limit": 10 } ``` **`artifact_get`** returns one artifact plus its relations (up to 25). Argument: `{ "artifact_id": "0199198a-8f21-7c9e-9b1a-4e2f6d1a0c33" }`. **`artifact_list`** filters a project's artifacts with a cursor, not a free-text query. | Parameter | Type | Default | |---|---|---| | `project_id` | UUID | required | | `status` | string[] | none | | `tag` | string | none | | `content_type` | string | none | | `external_key` | string | none | | `limit` | integer | `20` (capped at 25) | | `cursor` | string | none | ```json { "project_id": "3f5c...", "tag": "churn", "limit": 20 } ``` **`project_list`** takes no arguments; it returns every project the credential can use. ### Caching by name **`artifact_resolve`** turns "fetch only if nobody already has" into one call: `hit` (a fresh artifact exists, use it), `create` (you hold the reservation: `PUT` to `upload.url`, then `artifact_complete_upload`), or `pending` (retry after `retry_after_seconds`). | Parameter | Type | Default | |---|---|---| | `project_id` | UUID | required | | `external_key` | string | required | | `filename` | string | required | | `content_type` | string | required | | `size_bytes` | integer | required | | `title` | string | required | | `max_age_seconds` | integer | none (any age counts as fresh) | | `run_id` | string | none | ```json { "project_id": "3f5c...", "external_key": "company-api/acme/2026-09-17", "max_age_seconds": 86400, "filename": "acme.json", "content_type": "application/json", "size_bytes": 5120, "title": "Acme enrichment" } ``` ### Uploading **`artifact_create_upload`** reserves a new artifact and returns a signed, short-lived upload URL. `PUT` the bytes to `upload.url` with the given headers, then call `artifact_complete_upload`. | Parameter | Type | Default | |---|---|---| | `project_id` | UUID | required | | `filename` | string | required | | `content_type` | string | required | | `size_bytes` | integer | required | | `title` | string | required | | `description` | string | `""` | | `tags` | string[] | none | | `metadata` | object | none | | `external_key` | string | none | | `run_id` | string | none | | `infer_lineage` | boolean | `true` | Field sizes are limited; see [Limits](/docs/rest/#limits). ```json { "project_id": "3f5c...", "filename": "report.pdf", "content_type": "application/pdf", "size_bytes": 88213, "title": "Q3 churn analysis", "tags": ["churn", "q3"] } ``` **`artifact_complete_upload`** confirms the upload. The server verifies the object and marks it ready; the response says `processing` until that finishes, usually within a second or two; re-read with `artifact_get`. Safe to call more than once. ```json { "artifact_id": "0199198a-...", "version_id": "0199198b-...", "sha256": "e3b0c4..." } ``` **`artifact_create_version`** starts a new immutable version and returns another signed upload URL. On completion the server writes a `supersedes` relation to the previous version. | Parameter | Type | Default | |---|---|---| | `artifact_id` | UUID | required | | `filename` | string | required | | `content_type` | string | required | | `size_bytes` | integer | required | | `run_id` | string | none | | `infer_lineage` | boolean | `true` | ### Downloading **`artifact_get_download_url`** returns a short-lived signed URL; fetch it yourself. Records a usage event. | Parameter | Type | Default | |---|---|---| | `artifact_id` | UUID | required | | `version_id` | UUID | none (latest version) | | `run_id` | string | none | Argument: `{ "artifact_id": "0199198a-8f21-7c9e-9b1a-4e2f6d1a0c33" }`. ### Lineage and lifecycle **`artifact_add_relation`** states that one artifact relates to another: `derived_from`, `supersedes`, `attachment_of`, `generated_by` or `related_to`. Explicit relations replace inferred ones. | Parameter | Type | Default | |---|---|---| | `artifact_id` | UUID | required | | `to_artifact_id` | UUID | required | | `relation_type` | string | required | | `to_version_id` | UUID | none | | `metadata` | object | none | ```json { "artifact_id": "out-id", "to_artifact_id": "in-id", "relation_type": "derived_from" } ``` **`artifact_update_metadata`** changes title, description, tags, metadata or TTL. Bytes are immutable; use `artifact_create_version` for new content. Re-embeds when a searchable field changes. | Parameter | Type | Default | |---|---|---| | `artifact_id` | UUID | required | | `title` | string | none | | `description` | string | none | | `tags` | string[] | none | | `metadata` | object | none | | `expires_at` | string (date-time) | none | | `clear_expires_at` | boolean | `false` | **`artifact_delete`** moves the artifact to `deleting`; a worker removes the bytes. Not reversible. Argument: `{ "artifact_id": "0199198a-8f21-7c9e-9b1a-4e2f6d1a0c33" }`. ### The artifact resource `artefactai://artifacts/{artifact_id}` is a pointer, not a data endpoint. A resource read carries no principal to authorize against, so it returns only the id you gave it plus a nudge to call `artifact_get` or `artifact_get_download_url` instead. ## Typical agent flow A research agent hands a report to a writing agent, neither knowing where the other runs. 1. Research agent calls `artifact_create_upload`, then `PUT`s the bytes to `upload.url`. 2. Research agent calls `artifact_complete_upload`, then polls `artifact_get` until `status` is `ready`. 3. Writing agent, on a separate host, calls `artifact_search` (`hybrid`, same `project_id`) and picks a hit with `superseded: false` and `stale_upstream: false`. 4. Writing agent calls `artifact_get_download_url` and fetches the bytes itself. 5. Writing agent uploads its own output tagged with the same `run_id`; since it downloaded the first artifact in that run before uploading, the server infers a `derived_from` edge on its own, or either agent can call `artifact_add_relation` to state it explicitly. Source: https://artefaktum.dev/docs/mcp/ --- # REST API > The plain HTTP API behind the SDKs, for curl, Make, Zapier or any HTTP step. ## Base URL and authentication ``` https://api.artefaktum.dev ``` Every request except `/health/live` and `/health/ready` needs an API key, created in the [console](https://artefaktum.dev/console/), sent as: ``` Authorization: Bearer ``` Requests and responses are JSON: send `Content-Type: application/json` on any body. The OpenAPI document is at `https://api.artefaktum.dev/openapi.json`; interactive docs from it are at `https://api.artefaktum.dev/docs`, useful for a typed client in a language the SDKs don't cover. ## Limits Limits apply when you write. Artifacts that are already stored are never re-validated. | Field | Limit | |---|---| | `title` | 1 to 500 characters | | `description` | 10,000 characters | | `tags` | 50 per artifact | | one tag | 1 to 64 characters, not blank | | `metadata` | 16,384 bytes as compact JSON, nested at most 5 levels deep | | `external_key` | 512 characters | | `filename` | 1,024 characters | | `content_type` | 255 characters | | `run_id` | 128 characters | | request body | 262,144 bytes (256 KB) | A field over its limit answers `400 invalid_request`. `detail` starts with the field's path, then says what was sent and what the limit is, for example `body.metadata: Value error, metadata is 20011 bytes as JSON; the limit is 16384`. A tag's path carries its position in the list, as in `body.tags.0`. Branch on `code`, not `detail`: its wording can change. A request body over 256 KB answers `413 request_too_large` before it is read, for example `the request body exceeds the limit of 262144 bytes`. File bytes never pass through the API, so this limit does not apply to your files: those go straight to storage, up to the file size of your plan. Metadata depth counts containers: `{}` is one level, `{"a": {"b": 1}}` is two. Lists count the same as objects. Storage, monthly API calls and file size depend on your plan; see [pricing](/pricing/). ## Errors Errors are `application/problem+json` (RFC 9457): ```json { "type": "https://artefact.ai/errors/artifact_not_found", "title": "Artifact not found", "status": 404, "detail": "artifact 0199198a-... not found", "instance": "/v1/artifacts/0199198a-...", "code": "artifact_not_found", "request_id": "..." } ``` `code` is the stable field to branch on; `type`, `title` and `detail` are for humans. Codes you'll actually see: | Code | Status | Meaning | |---|---|---| | `invalid_request` | 400 | Body or query param failed validation. | | `unauthorized` | 401 | Missing or invalid bearer token. | | `insufficient_scope` | 403 | The key lacks the required scope. | | `artifact_not_found` | 404 | No such artifact, or it belongs to another tenant. | | `run_not_found` | 404 | No such run. | | `artifact_not_ready` | 409 | The artifact's state doesn't support this call. | | `external_key_conflict` | 409 | The external key is already used in this project. | | `idempotency_conflict` | 409 | `Idempotency-Key` reused with a different body. | | `run_sealed` | 409 | The run no longer accepts writes. | | `upload_expired` | 410 | The signed upload window closed before completion. | | `object_verification_failed` | 422 | The uploaded object failed verification. | | `quota_exceeded` | 413 | The file or tenant storage exceeds the plan limit. | | `quota_exceeded` | 429 | The monthly call limit is exhausted. | | `request_too_large` | 413 | The request body is over 256 KB. File bytes never count. | | `upstream_unavailable` | 502 | A dependency ArtefactAI calls did not answer. | | `embedding_unavailable` | 503 | The embedding provider could not produce a vector. | `quota_exceeded` is deliberately the same code at two statuses: clients branch on the code, and 429 (not 402) is used so it doesn't collide with a payment-required response. ## Endpoints | Method | Path | Scope | Purpose | |---|---|---|---| | GET | `/health/live` | none | Liveness probe. | | GET | `/health/ready` | none | Liveness plus a database check. | | GET | `/health/whoami` | any | Return the tenant and project the key resolves to. | | GET | `/v1/projects` | `artifacts:read` | List the projects this credential can use. | | POST | `/v1/artifacts/search` | `artifacts:search` | Find artifacts by text, semantic similarity or filters. | | POST | `/v1/artifacts/resolve` | `artifacts:write` | Reuse a fresh artifact by external key, or reserve it. | | POST | `/v1/artifacts/uploads` | `artifacts:write` | Reserve a new artifact; get a signed upload URL. | | POST | `/v1/artifacts/{artifact_id}/versions/{version_id}/complete` | `artifacts:write` | Confirm the bytes were uploaded. | | GET | `/v1/artifacts` | `artifacts:read` | List artifacts in a project, with filters and a cursor. | | GET | `/v1/artifacts/by-external-key/{external_key}` | `artifacts:read` | Look up an artifact by external key. | | GET | `/v1/artifacts/{artifact_id}` | `artifacts:read` | Read one artifact's metadata and lifecycle state. | | GET | `/v1/artifacts/{artifact_id}/versions` | `artifacts:read` | List an artifact's versions. | | GET | `/v1/artifacts/{artifact_id}/download` | `artifacts:read` | Get a short-lived signed download URL. | | PATCH | `/v1/artifacts/{artifact_id}` | `artifacts:write` | Update title, description, tags, metadata or TTL. | | POST | `/v1/artifacts/{artifact_id}/uploads` | `artifacts:write` | Start a new version; get a signed upload URL. | | POST | `/v1/artifacts/{artifact_id}/relations` | `artifacts:write` | Add an explicit lineage relation. | | GET | `/v1/artifacts/{artifact_id}/relations` | `artifacts:read` | List an artifact's direct relations, both directions. | | DELETE | `/v1/artifacts/{artifact_id}` | `artifacts:delete` | Request deletion (`202 Accepted`). | | POST | `/v1/runs` | `artifacts:write` | Create or register a run id. | | POST | `/v1/runs/{run_id}/seal` | `artifacts:write` | Reject further writes to a run. | | GET | `/v1/runs/{run_id}/artifacts` | `artifacts:read` | List artifacts produced in a run. | | POST | `/v1/api-keys` | `artifacts:admin` | Mint a key; the secret is returned once. | | GET | `/v1/api-keys` | `artifacts:admin` | List keys, never their secrets. | | DELETE | `/v1/api-keys/{key_id}` | `artifacts:admin` | Revoke a key. | | GET | `/v1/quota` | `artifacts:read` | Plan, limits and current usage for the tenant. | | GET | `/v1/usage` | `artifacts:admin` | Time-bucketed request and byte counts. | A key can never be granted a scope its creator lacks. ## Worked examples ### Search Exactly the call the homepage's REST panel shows: ```sh curl -s -X POST https://api.artefaktum.dev/v1/artifacts/search \ -H "Authorization: Bearer $ARTEFAKTUM_API_KEY" -H "Content-Type: application/json" \ -d '{"project_id": "…", "query": "latest enriched competitor list", "mode": "hybrid"}' ``` Request body fields: | Field | Type | Default | |---|---|---| | `project_id` | UUID | required | | `query` | string | `""` | | `mode` | `"exact"` \| `"text"` \| `"semantic"` \| `"hybrid"` | `"hybrid"` | | `filters.tags_all` | string[] | `[]` | | `filters.content_types` | string[] | `[]` | | `filters.status` | string[] | `["ready"]` | | `filters.external_key` | string | none | | `filters.created_after` / `filters.created_before` | date-time | none | | `filters.exclude_superseded` | boolean | `true` | | `limit` | integer, 1-200 | `20` | | `cursor` | string | none | Response items carry the artifact, a `score` and a `match_mode`; scores aren't comparable across modes. ### Create upload, PUT bytes, complete ```sh curl -s -X POST https://api.artefaktum.dev/v1/artifacts/uploads \ -H "Authorization: Bearer $ARTEFAKTUM_API_KEY" -H "Content-Type: application/json" \ -H "Idempotency-Key: $(uuidgen)" \ -d '{ "project_id": "'"$PID"'", "title": "Acme enrichment response", "filename": "acme.json", "content_type": "application/json", "size_bytes": 5120, "tags": ["company", "enrichment"] }' ``` Request body fields: | Field | Type | Default | |---|---|---| | `project_id` | UUID | required | | `filename` | string | required | | `content_type` | string | required | | `size_bytes` | integer | required | | `title` | string | required | | `description` | string | `""` | | `tags` | string[] | `[]` | | `metadata` | object | `{}` | | `external_key` | string | none | | `expires_at` | date-time | none | | `run_id` | string | none | | `infer_lineage` | boolean | `true` | `summary` was removed on 2026-09-27. A request that still sends the key is refused with `400 invalid_request`, like any other unknown key, and responses no longer carry it. Clients published before then (Python SDK 0.1.1, TypeScript SDK 0.1.0, n8n node 0.1.3) send the key only when you set a summary; stop setting it or upgrade. This is the one endpoint that accepts an `Idempotency-Key` header, so a retried request doesn't create a second reservation. Response: ```json { "artifact": { "id": "...", "version_id": "...", "status": "pending_upload" }, "upload": { "method": "PUT", "url": "...", "headers": {"content-type": "application/json"}, "expires_at": "..." } } ``` `PUT` the bytes to `upload.url` with the given headers, then complete: ```sh curl -s -X PUT "$UPLOAD_URL" -H "Content-Type: application/json" --data-binary @acme.json curl -s -X POST "https://api.artefaktum.dev/v1/artifacts/$AID/versions/$VID/complete" \ -H "Authorization: Bearer $ARTEFAKTUM_API_KEY" ``` Safe to repeat. The artifact is `ready` once the server verifies the object; embedding for semantic search happens afterwards, tracked separately as `semantic_ready`. ### Get a download URL ```sh curl -s "https://api.artefaktum.dev/v1/artifacts/$AID/download" \ -H "Authorization: Bearer $ARTEFAKTUM_API_KEY" ``` Optional params: `version_id` (defaults to latest) and `run_id` (for lineage inference on a later upload). The response is a short-lived signed URL; the API never proxies the bytes, and fetching it records a usage event. ### Resolve by external key ```sh curl -s -X POST https://api.artefaktum.dev/v1/artifacts/resolve \ -H "Authorization: Bearer $ARTEFAKTUM_API_KEY" -H "Content-Type: application/json" \ -d '{ "project_id": "'"$PID"'", "external_key": "company-api/acme/2026-09-17", "max_age_seconds": 86400, "filename": "acme.json", "content_type": "application/json", "size_bytes": 5120, "title": "Acme enrichment" }' ``` Request body fields: | Field | Type | Default | |---|---|---| | `project_id` | UUID | required | | `external_key` | string | required | | `max_age_seconds` | integer | none (any existing artifact counts as fresh) | | `filename` | string | required | | `content_type` | string | required | | `size_bytes` | integer | required | | `title` | string | required | | `description` | string | `""` | | `tags` | string[] | `[]` | | `metadata` | object | `{}` | | `run_id` | string | none | `status` is `hit` (an artifact is returned, no upload needed), `create` (you hold the reservation: `PUT` to `upload.url`, then complete), or `pending` (retry after `retry_after_seconds`). For a plain lookup with no side effect, use `GET /v1/artifacts/by-external-key/{external_key}?project_id=...`. ### Add a relation ```sh curl -s -X POST "https://api.artefaktum.dev/v1/artifacts/$OUT_ID/relations" \ -H "Authorization: Bearer $ARTEFAKTUM_API_KEY" -H "Content-Type: application/json" \ -d '{"to_artifact_id": "'"$IN_ID"'", "relation_type": "derived_from"}' ``` Body: `to_artifact_id` (required), `relation_type` (required: `derived_from`, `supersedes`, `attachment_of`, `generated_by` or `related_to`), optional `to_version_id`, optional `metadata`. Explicit relations replace inferred ones. When an upload names a `run_id`, the server infers `derived_from` edges from whatever that key downloaded earlier in the run; pass `"infer_lineage": false` on the upload to suppress it. ## Quota and usage `GET /v1/quota` never counts against the monthly call limit, so a key can always see why it was refused. It returns the plan name, `storage: {used_bytes, limit_bytes}`, `calls: {used, limit, resets_at}`, and `max_file_bytes`. `GET /v1/usage` takes `start`, `end`, `granularity` (`hour` or `day`), `metric`, `project_id` and `principal_id`, returning time-bucketed counts for `requests`, `requests_error`, `storage_bytes`, `bytes_uploaded` and `bytes_downloaded`. Source: https://artefaktum.dev/docs/rest/ --- # Python SDK > Push, pull, search, and manage artifacts from Python with the official artefaktum package ## Install ```sh pip install artefaktum ``` Python 3.10+. The only dependency is `httpx`. For the CLI, see [CLI](/docs/cli/). ## Configure Resolved in order: constructor argument, then environment variable, then default. | Argument | Environment variable | Default | |---|---|---| | `api_key` | `ARTEFAKTUM_API_KEY` | *(required)* | | `base_url` | `ARTEFAKTUM_BASE_URL` | `https://api.artefaktum.dev` | | `project` | `ARTEFAKTUM_PROJECT` | `"default"` | ```python client = Artefaktum(api_key="ak_...", base_url="http://localhost:3000", project="default", timeout=30.0) ``` API keys are created in the [console](/console/). `MissingApiKey` (a `ValueError`) is raised locally, before any request, if no key is found anywhere. `project` is a slug or a UUID. A slug is resolved to an ID through `GET /v1/projects` once, then cached. `push`, `search`, `list`, `resolve`, and a few others also take a per-call `project=` override. ## Five-minute example ```python from artefaktum import Artefaktum client = Artefaktum() # ARTEFAKTUM_API_KEY, project "default" a = client.artifacts.push("report.pdf", title="Q3 churn", tags=["churn"]) hits = client.artifacts.search("q3 churn") client.artifacts.pull(hits.items[0].artifact.id, "out/") # sha256 verified ``` ## Pushing `push` hashes, reserves, uploads, and (by default) waits for the artifact to be ready: 1. Hashes and sizes the source locally, streamed; guesses a content type from the filename unless one is given. 2. Reserves an upload (`create_upload`), returning a signed PUT URL. 3. Uploads the bytes directly to object storage, over a client with no `Authorization` header, so the API key never reaches the storage host. 4. Marks the version complete (`complete_upload`) with the digest and size. 5. By default, polls every 0.5s until `ready`, raising `ProcessingFailed` or `ProcessingTimeout`. `wait=False` returns right after step 4, with status `processing` or `ready`. ```python client.artifacts.push( "report.pdf", title="Q3 churn", description="Churn breakdown by segment", tags=["churn", "q3"], external_key="reports/q3-churn", ) ``` | Argument | Default | Meaning | |---|---|---| | `source` | required | path (streamed) or `bytes` (needs `filename`) | | `title` | required | artifact title | | `description` | `""` | | | `tags` | `()` | sequence of tags | | `metadata` | `None` | mapping of custom fields | | `external_key` | `None` | key for `resolve` / `get_by_external_key` | | `expires_at` | `None` | `datetime` after which the artifact expires | | `run` | `None` | attach to a run ID | | `infer_lineage` | `True` | let the server infer relations | | `content_type` | `None` | guessed from `filename` if omitted | | `filename` | `None` | required for `bytes`; else the path's name | | `wait` | `True` | poll until `ready` | | `timeout` | `30.0` | seconds to wait | | `project` | `None` | override the default project | Field sizes are limited; see [Limits](/docs/rest/#limits). `create_version(artifact_id, source, ...)` runs the same flow for a new version of an existing artifact: `run`, `infer_lineage`, `content_type`, `filename`, `wait`, `timeout`, but no title/tags/metadata, since those belong to the artifact, not the version. ## Pulling ```python path = client.artifacts.pull(artifact_id, "out/") ``` `pull(artifact_id, dest, *, verify=True)` streams the latest version to `dest` and returns the final `Path`. `dest` is a directory when it already is one, or when it is a *string* ending in `/`, which `pull` creates. A `pathlib.Path` cannot express that intent, since `Path("out/")` is just `Path("out")`. Into a directory, the file is named after the version's stored filename, reduced to a basename, so an uploader cannot steer the write outside it. The download runs on a client with no `Authorization` header. `verify=True` (the default) checks the sha256 digest and raises `IntegrityError` on a mismatch, deleting the partial file; a version the server recorded no digest for is written unverified regardless. `verify=False` skips the check. ## Finding ```python hits = client.artifacts.search("q3 churn", mode="hybrid", tags_all=["churn"]) page = client.artifacts.list(tag="churn", limit=50) one = client.artifacts.get(artifact_id) one = client.artifacts.get_by_external_key("reports/q3-churn") ``` `search` returns a `SearchPage` of `SearchHit` (`.artifact`, `.score`, `.match_mode`). Key arguments: `query`, `mode` (`hybrid` default, or `text`, `semantic`, `exact`), `limit` (20), `cursor`, `content_types`, `tags_all`, `status` (default `("ready",)`), `external_key`, `created_after`/`created_before`, `exclude_superseded` (`True` by default), `project`. `list` returns a `Page[Artifact]`, with the same filters as `search` minus `query`/`mode`, plus `expires_before` and default `limit=50`. `iter_all(...)` takes the same filters and is a generator that follows `next_cursor` page by page, yielding every matching `Artifact`; on the async client, `iter_all` is an async generator instead. `get(artifact_id)` and `get_by_external_key(key, *, project=None)` each return a single `Artifact`, raising `NotFound` if there is none. ## Resolving and caching by external key `resolve` is get-or-create for an external key, so a run avoids re-uploading something another run already produced: ```python resolution = client.artifacts.resolve( "reports/q3-churn", filename="report.pdf", content_type="application/pdf", size_bytes=len(data), title="Q3 churn", max_age=timedelta(days=30), ) if resolution.status == "create": artifact = client.artifacts.fulfil(resolution, "report.pdf") else: artifact = resolution.artifact # status == "hit" ``` `max_age` (`int`, `float`, or `timedelta` seconds, sent as `max_age_seconds`) treats an older artifact as stale, so `resolve` returns `status="create"` instead of `"hit"`. `Resolution.status`: `hit` (`.artifact` set, nothing uploaded), `create` (`.reservation`/`.upload` set; pass the resolution and bytes to `fulfil(resolution, source, *, filename=None, wait=True, timeout=30.0)`, which runs `push`'s upload flow), or `pending` (another writer holds the reservation; `.retry_after_seconds` hints the wait). `fulfil` raises `ValueError` for any other status. ## Versions and relations ```python versions = client.artifacts.versions(artifact_id) relation = client.artifacts.add_relation(artifact_id, other_id, "derived_from") relations = client.artifacts.relations(artifact_id) client.artifacts.update(artifact_id, title="New title", tags=["a", "b"]) client.artifacts.delete(artifact_id) ``` `versions(artifact_id)` returns every `Version`, newest last. `add_relation(artifact_id, to_artifact_id, relation_type, *, to_version_id=None, metadata=None)` requires `relation_type` to be `derived_from`, `supersedes`, `attachment_of`, `generated_by`, or `related_to`, else `ValueError`. `relations(artifact_id)` lists relations both ways. `update(...)` patches `title`, `description`, `tags`, `metadata`, `expires_at`, `clear_expires_at` without touching the file. `delete(artifact_id)` removes an artifact. ## Async client `AsyncArtefaktum` mirrors every method on `Artefaktum` as a coroutine; `iter_all` becomes an async generator: ```python import asyncio from artefaktum import AsyncArtefaktum async def main(): async with AsyncArtefaktum() as client: a = await client.artifacts.push("report.pdf", title="Q3 churn") async for artifact in client.artifacts.iter_all(tag="churn"): print(artifact.id) asyncio.run(main()) ``` ## Errors Every failure is an `ArtefaktumError(message, code, status, request_id)` or a subclass: ```python from artefaktum import Artefaktum, NotFound, ArtefaktumError with Artefaktum() as client: try: client.artifacts.get("01a0be89-1cde-74c1-ab17-8814cc9d9141") except NotFound as e: print(e.code, e.request_id) # "artifact_not_found", "req_..." except ArtefaktumError as e: print(str(e)) # ": (request_id=)" ``` | Exception | Code(s) | Status | When | |---|---|---|---| | `NotFound` | `artifact_not_found`, `run_not_found`, `not_found` | 404 | no such artifact or run | | `Unauthorized` | `unauthorized` | 401 | bad API key | | `Forbidden` | `insufficient_scope` | 403 | key lacks scope | | `QuotaExceeded` | `quota_exceeded` | 413, 429 | 413: file/storage too large; 429: calls spent | | `RequestTooLarge` | `request_too_large` | 413 | the JSON request is over 256 KB | | `Conflict` | `artifact_not_ready`, `external_key_conflict`, `idempotency_conflict`, `run_sealed` | 409 | server-side conflict | | `ValidationFailed` | `invalid_request` | 400 | malformed request | | `UploadError` | `upload_expired`, `object_verification_failed` | 410, 422 | upload window passed, or verification failed | | `ServiceUnavailable` | `embedding_unavailable` | 503 | embeddings unavailable | | `ProcessingFailed` | `processing_failed` | n/a | wait saw status `failed`; carries `.artifact` | | `ProcessingTimeout` | `processing_timeout` | n/a | wait exceeded `timeout`; carries `.artifact` | | `IntegrityError` | `integrity_error` | n/a | `pull` sha256 mismatch; `.expected`/`.actual` | | `StorageError` | `storage_error` | n/a | storage host failed; carries `.host` | | `MissingApiKey` | n/a | n/a | raised locally, before any request | A 429 `QuotaExceeded` blocks writes and `search` until the plan resets; reads, downloads, and deletes keep working. `client.quota()` reports the plan, limits, and usage, and is never blocked: ```python q = client.quota() print(q.plan, q.storage.used_bytes, "/", q.storage.limit_bytes, q.calls.resets_at) ``` ## Retries and timeouts Reads retry automatically on HTTP 429, 502, 503, or 504, and on connection errors, up to 3 attempts, waiting 0.5s then 1s (or the server's `Retry-After`, capped at 10s): `get`, `get_by_external_key`, `list` / `iter_all`, `search`, `download_url`, `whoami`, `projects.list`, `usage.get`. `search` is a POST but has no side effects, so it retries too. A 429 coded `quota_exceeded` is the exception, raised immediately since the allowance returns only at `quota().calls.resets_at`. Writes (`push`, `update`, `add_relation`, `delete`, `keys.create`, and the rest) are never retried automatically, so a client never double-submits one; neither is the storage upload or download. The `timeout` constructor argument (default `30.0`) sets the HTTP timeout on both the API and storage clients. The separate `timeout=` on `push`, `create_version`, and `fulfil` controls only how long the ready-poll waits, independent of the HTTP timeout. Source: https://artefaktum.dev/docs/python/ --- # TypeScript SDK > Install, configure and use the artefaktum npm package: push, pull, search and resolve artifacts ## Install ```sh npm install artefaktum ``` Zero runtime dependencies, Node 20+, ESM and CommonJS builds. Types are generated from the API's own OpenAPI document, so a server-side rename is a compile error, not a runtime surprise. ```ts import { Artefaktum } from "artefaktum"; // CommonJS: const { Artefaktum } = require("artefaktum"); ``` ## Configure Create an API key in the [console](/console/). | Option | Environment | Default | |---|---|---| | `apiKey` | `ARTEFAKTUM_API_KEY` | required; throws `MissingApiKeyError` if missing | | `baseUrl` | `ARTEFAKTUM_BASE_URL` | `https://api.artefaktum.dev` | | `project` | `ARTEFAKTUM_PROJECT` | `"default"` | | `timeoutMs` | none | `30000` per API call; storage transfers have no timeout | | `fetch` | none | `globalThis.fetch` | `project` accepts a UUID or slug; a slug is resolved once and cached. Every project-scoped method also takes `project` to override it for one call. Methods are camelCase; fields are snake_case, spelled as the REST API, MCP tools and Python SDK spell them (`external_key`, `latest_version.size_bytes`, `stale_upstream`). Timestamps accept an ISO 8601 string or a `Date`; durations name their unit (`timeoutMs`, `max_age_seconds`). ## Example ```ts import { Artefaktum } from "artefaktum"; const client = new Artefaktum(); // reads ARTEFAKTUM_API_KEY; project "default" const artifact = await client.artifacts.push("report.pdf", { title: "Q3 churn analysis", tags: ["churn", "q3"], external_key: "reports/q3-churn", }); const page = await client.artifacts.search("q3 churn"); for (const hit of page.items) console.log(hit.score, hit.artifact.title); await client.artifacts.pull(artifact.id, "out/"); // → out/report.pdf, sha256-verified ``` ## Uploading `push(source, options)` hashes the source, reserves an upload, PUTs the bytes directly to object storage (your API key never reaches that host), completes it, and by default waits until the artifact is `ready`. ```ts await client.artifacts.push("data/export.parquet", { title: "Export" }); // path (Node): streamed await client.artifacts.push(bytes, { title: "Export", filename: "export.json" }); // Uint8Array | ArrayBuffer | Blob ``` `source` is a file path (Node only) or bytes. Bytes need `filename` unless the `Blob` is a named `File`; `content_type` defaults to the Blob's type, else it's guessed from the name. | Option | Meaning | |---|---| | `title` | Required. What the artifact is. | | `description`, `tags`, `metadata` | Description, tag list, arbitrary JSON metadata. | | `external_key` | Your own unique key for this artifact within the project. | | `expires_at` | ISO string or `Date`; the artifact is deleted after this time. | | `run`, `infer_lineage` | Link to a run id; `infer_lineage` (default `true`) links what the run read. | | `wait`, `timeout_ms`, `signal` | Wait for `ready` (default `true`, 30 000 ms); `signal` cancels the transfer. | Field sizes are limited; see [Limits](/docs/rest/#limits). Storage transfers have no timeout of their own, since a total timeout would kill a large upload. Pass `signal` for a bound, e.g. `AbortSignal.timeout(10 * 60_000)`. A failed PUT throws `StorageError` and leaves the artifact `pending_upload`, never `ready` and invisible to `search`, so it is safe to retry. With `wait: true`, a processing failure throws `ProcessingFailedError` or `ProcessingTimeoutError`, both carrying `.artifact`. `createVersion(artifact_id, source, options)` adds a version through the same flow (`run`, `infer_lineage`, `content_type`, `filename`, plus the wait options). ### Compute once, reuse everywhere `resolve` and `fulfil` cache an expensive computation under an `external_key`, so a later caller gets the stored result instead of recomputing it. ```ts const r = await client.artifacts.resolve("openweather/vilnius/2026-09-21", { filename: "weather.json", content_type: "application/json", size_bytes: body.byteLength, title: "Vilnius weather", max_age_seconds: 3600, }); if (r.status === "hit" && r.artifact) use(r.artifact); else if (r.status === "create") await client.artifacts.fulfil(r, body); else await sleep((r.retry_after_seconds ?? 2) * 1000); // "pending": someone else is producing it ``` `resolve(external_key, options)` requires `filename`, `content_type`, `size_bytes`, `title`, and takes `description`, `tags`, `metadata`, `max_age_seconds`, `run`. `status` is `"hit"` (fresh artifact in `.artifact`), `"create"` (call `fulfil` with this resolution and the bytes), or `"pending"` (someone else is producing it; back off using `.retry_after_seconds`). `fulfil` throws `TypeError` unless `status` is `"create"`. ## Downloading ```ts const path = await client.artifacts.pull(id, "out/"); // Node: streams to disk, verifies sha256 const { data, filename } = await client.artifacts.pullBytes(id); // any runtime: verified bytes in memory ``` `pull` is Node-only; `dest` is a directory when it exists as one or ends with a separator (then created), otherwise the file to write. `pullBytes` works on any runtime, returning `{ data, filename, content_type, version_id, sha256 }`. Both take `{ verify?, signal? }`; `verify` defaults to `true` and a mismatch throws `IntegrityError`, leaving no file behind. `downloadUrl(artifact_id, { version_id?, run? })` returns the signed URL directly. ## Finding ```ts await client.artifacts.search("emission factors", { mode: "hybrid", tags_all: ["ghg"] }); await client.artifacts.getByExternalKey("reports/q3-churn"); for await (const a of client.artifacts.iterAll({ tag: "churn" })) console.log(a.title); ``` | Method | Purpose | |---|---| | `search(query, options)` | Ranked results. `mode`: `"hybrid"` (default), `"semantic"`, `"text"`, `"exact"`. Also `content_types`, `tags_all`, `status`, `external_key`, `created_after`/`before`, `exclude_superseded`, `limit` (20), `cursor`, `project`. Returns `{ items: [{ score, artifact }], next_cursor }`. | | `get(artifact_id)` | Fetch one artifact by id. | | `getByExternalKey(key, { project? })` | Fetch by external key. | | `list(options)` | One page: `status`, `content_type`, `tag`, `external_key`, `created_before`/`after`, `expires_before`, `limit` (50), `cursor`. | | `iterAll(options)` | Async generator over every match, following `next_cursor`. | Search hides superseded artifacts by default (`exclude_superseded`); each result reports whether it is `superseded` or `stale_upstream`. ## Versions and relations ```ts await client.artifacts.versions(artifact_id); await client.artifacts.relations(artifact_id); await client.artifacts.addRelation(artifact_id, to_artifact_id, "derived_from"); ``` `versions` lists an artifact's versions; `relations` lists its relations to other artifacts. `addRelation(artifact_id, to_artifact_id, relation_type, { to_version_id?, metadata? })` requires `relation_type` to be one of `derived_from`, `supersedes`, `attachment_of`, `generated_by`, `related_to`; anything else throws `TypeError` first. `update(artifact_id, options)` patches `title`, `description`, `tags`, `metadata`, `expires_at`; only fields passed are sent, and `clear_expires_at: true` removes the expiry. `delete(artifact_id)` is accepted with 202; bytes are removed in a background job. Other namespaces: `client.projects.list()`; `client.runs.create`/`.seal`/`.artifacts` for grouping by pipeline run; `client.keys`, `client.usage` (admin scope); `client.whoami()`, `client.quota()`. ## Errors Every failure from the API, storage, or the SDK's own helpers is an `ArtefaktumError` with `code`, `message`, `status`, `request_id` (quote it in support mail). `MissingApiKeyError` extends plain `Error`. Caller mistakes the compiler can't catch in JavaScript throw `TypeError`: `fulfil` on a non-`create` resolution, an unknown `relation_type`, nameless bytes without `filename`, an invalid `baseUrl`. | Class | `code` | |---|---| | `NotFoundError` | `artifact_not_found`, `run_not_found`, `not_found`, `project_not_found` | | `UnauthorizedError` / `ForbiddenError` | `unauthorized` / `insufficient_scope` | | `QuotaExceededError` | `quota_exceeded` | | `RequestTooLargeError` | `request_too_large` | | `ConflictError` | `artifact_not_ready`, `external_key_conflict`, `idempotency_conflict`, `run_sealed` | | `ValidationError` | `invalid_request` | | `UploadError` | `upload_expired`, `object_verification_failed` | | `ServiceUnavailableError` | `embedding_unavailable` | | `ConnectionError` | `connection_error`: API unreachable after retries | | `StorageError` | `storage_error`: object storage refused or was unreachable | | `ProcessingFailedError` / `ProcessingTimeoutError` | `processing_failed` / `processing_timeout` (both carry `.artifact`) | | `IntegrityError` | `integrity_error`: downloaded bytes don't match the recorded sha256 | | `ArtefaktumError` | `version_mismatch`: the artifact gained a version mid-pull; retry | | `ArtefaktumError` | `invalid_filename`, `unsupported_runtime` (a file path on a runtime without `fs`), `http_error` (non-JSON response) | ```ts try { await client.artifacts.get(id); } catch (err) { if (err instanceof ArtefaktumError && err.code === "artifact_not_found") { /* … */ } else throw err; } ``` Prefer `err.code` to `instanceof` in libraries: a program loading both the ESM and CommonJS build sees distinct classes. Reads (every `GET`, and `search`) retry up to twice on `429`/`502`/`503`/`504` and network failures, honouring `Retry-After` up to a 10 s ceiling. Writes, storage transfers and spent quota are never retried. ## Runtimes Tested on Node 20 and 22. The core uses only `fetch` and Web Crypto, so it should also run on Bun, Deno and Cloudflare Workers. Pass bytes or a `Blob` to `push` and use `pullBytes` there, since file paths need Node's `fs`. Browsers are not supported: an API key does not belong in one. See also [Quickstart](/docs/quickstart/) and [REST](/docs/rest/). Source: https://artefaktum.dev/docs/typescript/ --- # CLI > Push, pull, and search artifacts from a terminal, a Dockerfile, or a CI step with afk ## Install ```sh pip install "artefaktum[cli]" ``` This installs `artefaktum`, and the shorter alias `afk`, both pointing at the same command. It needs Python 3.10+, same as the SDK it wraps. ## Authenticate ```sh artefaktum login # or set ARTEFAKTUM_API_KEY in CI and sandboxes ``` `login` prompts for the key at a hidden prompt (or reads it from stdin with `--api-key-stdin`), verifies it against the server, then saves it to `~/.config/artefaktum/config.json` (`$XDG_CONFIG_HOME`, or `%APPDATA%\artefaktum` on Windows), mode `0600`. The key is never taken as a positional argument, so it never lands in shell history. `logout` removes the saved config file. For every setting (the API key, `--base-url`, `--project`), precedence is: the matching flag, then the matching environment variable (`ARTEFAKTUM_API_KEY`, `ARTEFAKTUM_BASE_URL`, `ARTEFAKTUM_PROJECT`), then the saved config file. In CI and sandboxes, prefer `ARTEFAKTUM_API_KEY` over `login` (no writable home directory needed) or `--api-key` (visible in `ps` to other users on the host). `whoami` shows the tenant, and project if the key is scoped to one: ```sh artefaktum whoami ``` API keys themselves are created in the console at https://artefaktum.dev/console/. The CLI has no `keys create` (or any `keys`/`usage` command); it only consumes a key that already exists. ## Commands | Command | Purpose | |---|---| | `whoami` | show the tenant (and project) the key belongs to | | `projects` | list the tenant's projects | | `login` | verify an API key and save it | | `logout` | remove the saved API key | | `get` | show one artifact, by ID or `--key` | | `ls` | list artifacts | | `search` | search artifacts | | `push` | upload a file as a new artifact | | `pull` | download an artifact's latest version | | `rm` | delete an artifact | | `link` | print a short-lived signed download URL | | `relations` | list an artifact's relations | | `relate` | record a relation from one artifact to another | | `versions` | list an artifact's versions | | `resolve` | get-or-create an artifact by external key | | `run new` | create a run | | `run seal` | seal a run so no more artifacts attach to it | | `run ls` | list a run's artifacts | Global options, given before the command: `--api-key`, `--base-url`, `--project`, `--json` / `--table` (force the output format), plus `--version` and `-h`/`--help`. `artefaktum --help` and `artefaktum --help` list every command and flag. ## Command reference **`get ID`** / **`get --key KEY`**: show one artifact. Exactly one of `ID` or `--key EXTERNAL_KEY` is required. **`ls`**: list artifacts. `--tag TAG`, `--status STATUS` (repeatable), `--limit N` (default 50), `--all` (follow every page instead of one). **`search QUERY`**: `--mode hybrid|text|semantic|exact` (default `hybrid`), `--tag TAG` (repeatable, required tag), `--limit N` (default 10), `--history` (include superseded artifacts, excluded by default). **`push FILE`**: `--title` (required), `--tag` (repeatable), `--description`, `--key EXTERNAL_KEY`, `--meta K=V` (repeatable), `--expires DURATION` (e.g. `30d`), `--run RUN_ID`, `--no-wait` (return once accepted, don't wait for `ready`), `--timeout SECONDS` (default 30). Field sizes are limited; see [Limits](/docs/rest/#limits). **`pull ID [DEST]`**: `DEST` defaults to `.`; a trailing `/` creates it as a directory. `--no-verify` skips the sha256 check. **`rm ID`**: delete an artifact. No flags. **`link ID`**: print a signed download URL. `--table` mode prints exactly the URL and nothing else. **`relations ID`** / **`versions ID`**: list an artifact's relations or versions. No flags. **`relate ID TARGET`**: `--type` (`derived_from` (default), `supersedes`, `attachment_of`, `generated_by`, or `related_to`). **`resolve KEY`**: `--file` (required), `--title` (required), `--tag` (repeatable), `--description`, `--meta K=V` (repeatable), `--max-age DURATION` (an existing artifact older than this is treated as stale), `--wait SECONDS` (default 60, retried while another writer holds the reservation). Prints `hit` (nothing uploaded), `created` (uploaded `--file`), or exits `4` if still `pending` after `--wait`. **`run new [RUN_ID]`**: `RUN_ID` is optional; the server assigns one if omitted. **`run seal RUN_ID`** / **`run ls RUN_ID`**: `run ls` takes `--limit N` (default 50). Duration flags (`--expires`, `--max-age`) accept `90s`, `30m`, `12h`, `30d`, `2w`, or a bare integer of seconds. ## Exit codes | Code | Meaning | |---|---| | 0 | ok | | 1 | API or SDK error | | 2 | usage error | | 3 | not found | | 4 | `resolve` still pending when `--wait` ran out | | 130 | interrupted | ## Recipes **Dockerfile or sandbox image**: bake the CLI in and read the key from the environment at run time, never at build time. ```dockerfile RUN pip install "artefaktum[cli]" # ARTEFAKTUM_API_KEY is supplied by the container's runtime environment, not baked in ``` **GitHub Actions step:** ```yaml - run: pip install "artefaktum[cli]" - run: afk push report.pdf --title "Q3 churn" --tag churn env: ARTEFAKTUM_API_KEY: ${{ secrets.ARTEFAKTUM_API_KEY }} ``` **Push before a container is recycled**: an agent working in a sandbox or CI job that's about to be torn down should push its output as its last step, so the artifact outlives the container. ```sh afk push /tmp/output.json --title "Run output" --run "$RUN_ID" ``` **Scripting with JSON**: output is an aligned text table on a terminal and one JSON document otherwise, so a plain pipe already gets JSON. ```sh artefaktum ls | jq -r '.items[].id' # one page artefaktum ls --all | jq -r '.[].id' # every match, as a bare array ``` Pass `--json` explicitly when the command's stdout might still look like a terminal to it (for example, inside another program that captures output), rather than relying on the auto-detection. **Push only if it isn't already there**, without `resolve`: ```sh artefaktum get --key vendor-x/report/2026-09 >/dev/null \ || artefaktum push report.json --title "Vendor X report" --key vendor-x/report/2026-09 ``` Source: https://artefaktum.dev/docs/cli/ --- # n8n node > Use the Artefaktum community node in n8n to upload, cache, find and manage artifacts ## Status The node is published on npm and submitted to n8n for community node verification. Until n8n approves it, it does not appear in the Community Nodes catalogue or on n8n Cloud. The verified release is expected soon; in the meantime it installs on self-hosted n8n as below. ## Install On self-hosted n8n, either install it by package name under **Settings → Community Nodes**, or: ```sh npm install n8n-nodes-artefaktum ``` This adds node **Artefaktum** and credential **Artefaktum API**. ## Credentials Mint a key in the [console](/console/) and paste it into the credential's **API Key** field. Grant only the scopes needed: `read`, `write`, `search`, `delete` (only if deleting). **Base URL** defaults to `https://api.artefaktum.dev`; change only for self-hosting. ## Operations ### Artifact | Operation | Purpose | |---|---| | Upload | Store a new artifact from a binary property or text/JSON. | | Get or Upload | Return a cached artifact under an external key if fresh enough, else upload and cache it. | | Download | Download an artifact's file into a binary property. | | Get | Fetch one artifact by ID or external key. | | Get Many | Search or list artifacts. | | Update | Replace title, description, tags, metadata or expiry. | | Delete | Delete an artifact by ID. | ### Project | Operation | Purpose | |---|---| | Get Many | List the projects in your tenant; no parameters. | **Project**'s "From List" mode lists your account's projects; every account also has slug `default`. ## Upload | Parameter | Notes | |---|---| | Project | The owning project. | | Input Data Source | Binary property, or text/JSON via expression. | | Input Binary Field | Binary property holding the file. | | Content | Text to store (text source). | | File Name | Defaults to the binary or a generated name. | | Content Type | Defaults to the binary type, or `text/plain`. | | Title | Searched by agents; defaults to the file name. | | Options → Description | Searched semantically. | | Options → Expires In (Hours) | Auto-delete after N hours; `0` keeps it. | | Options → External Key | Your own unique key within the project. | | Options → Metadata (JSON) | Filterable structured metadata. | | Options → Tags | Comma-separated. | Node versions up to 0.1.3 still show an Options → Summary field. Leave it empty: the API refuses an upload that sets it. Version 0.2.0 removes the field. Field sizes are limited; see [Limits](/docs/rest/#limits). Upload returns the artifact with `status: "processing"` for a few seconds while search metadata is derived; Get Many only finds it once `ready`. ## Get or Upload Same fields as Upload, plus: | Parameter | Notes | |---|---| | External Key | A fresh-enough artifact with this key is returned instead of uploading. | | Max Age (Seconds) | Reuse only if younger; `0` accepts any age. | Output carries `cache: "hit"` or `cache: "created"`. ## Download | Parameter | Notes | |---|---| | Artifact ID | E.g. from an earlier Artefaktum node. | | Download Options → File Name | Override the output name. | | Download Options → Put Output File in Field | Target binary property. | | Download Options → Verify Checksum | Compare bytes to the stored SHA-256. | | Download Options → Version ID | Defaults to latest. | ## Get | Parameter | Notes | |---|---| | Lookup | By ID or by External Key. | | Artifact ID | Lookup = By ID. | | External Key | Lookup = By External Key. | | Project | Resolves the key (Lookup = By External Key). | | Simplify | Simplified vs. raw response; on by default. | ## Get Many | Parameter | Notes | |---|---| | Project | Project to search or list within. | | Query | Free text; empty lists by filters. | | Search Mode | Hybrid, Semantic or Text. | | Return All | Vs. a fixed Limit. | | Limit | Max results. | | Simplify | Simplified vs. raw response; on by default. | | Filters → Content Types | Comma-separated MIME types. | | Filters → Created After / Before | Date range. | | Filters → Include Superseded | Include superseded artifacts. | | Filters → Tags (All Of) | Comma-separated; every tag must match. | No sort option: the API ranks results itself. ## Update | Parameter | Notes | |---|---| | Artifact ID | The artifact to update. | | Update Fields → Title, Description, Tags, Metadata (JSON) | Leave blank to keep unchanged; clearing back to empty isn't supported yet. | | Update Fields → Expires At | Set a new expiry. | | Update Fields → Clear Expiry | Remove the expiry. | ## Delete Takes **Artifact ID** only. ## Recipe: caching a third-party API response **Get or Upload** de-duplicates an expensive call (an HTTP request, an LLM generation, a report render) by keying its result so later runs, here or elsewhere, reuse it. Give it an **External Key** such as `weather:vilnius:2026-09-23` and a **Max Age (Seconds)**. A fresh-enough artifact under that key returns as-is (`cache: "hit"`); otherwise your content uploads and is stored under it (`cache: "created"`). The key isn't workflow-scoped, so another workflow using it gets the cache too. A worked example, Manual Trigger → HTTP Request → Get or Upload caching an hour under a per-day key, ships as [`examples/cache-api-response.json`](https://github.com/artefaktum-dev/n8n-nodes-artefaktum/blob/main/examples/cache-api-response.json) in the repo. Get or Upload always needs the content on hand, since the API requires `size_bytes`. To skip that, call **Get** by external key first and branch on success. ## Limits Per-plan quotas apply (storage, rate, artifact count; see [pricing](/pricing/)), plus a per-file limit: 100 MB on Free, 5 GB on Pro. An exceeded quota returns `quota_exceeded`. The node's memory use is the tighter constraint on small instances: it buffers each file fully, roughly 2-3x its size per item. Keep files to tens of MB and avoid large batches under limited memory. ## Compatibility Requires n8n 1.0+. Tested against `n8nio/n8n:latest` (September 2026) and Node.js 20, 22, 24. No runtime dependencies. See also the [TypeScript SDK](/docs/typescript/), the [REST API](/docs/rest/), and the [full README](https://github.com/artefaktum-dev/n8n-nodes-artefaktum). Source: https://artefaktum.dev/docs/n8n/