Artifacts
Part 4 of the tutorial introduces jobs, schedules, and code repository images. This page explains another infrastructure concept that often appears right after that: Artifacts.
An Artifact is the platform's file-storage primitive. It stores and retrieves
files such as spreadsheets, CSV drops, reports, model binaries, or other binary
payloads.
Quick Summary
In this guide, you will:
- understand what an
Artifactis and when to use it - upload files into platform buckets from the Python client
- retrieve those files later from jobs and other platform consumers
- decide who can see, download and replace an Artifact
- decide who can browse a bucket and upload into it
- avoid common mistakes around duplicate uploads and fragile local paths
Mental model
Think of an Artifact as a file with a stable platform identity:
- a bucket
- a name
- the content
- a creation timestamp
That gives you a reference that survives beyond your laptop or a temporary network share.
In practical terms:
- local path: where the file happens to live today
- Artifact: how the platform knows that file tomorrow
Buckets and Artifacts are owned by an Organization Environment. SDK operations use the current registered branch's Environment context; callers do not provide an Environment UID or branch UID.
When Artifacts are the right tool
Artifacts are a good fit when the natural unit of work is still a file.
Common examples:
- a vendor drops daily
.csvor.xlsfiles into a folder - a job produces a PDF, HTML report, or model pickle
- a workflow needs a manually curated spreadsheet as input
Basic lifecycle
Most Artifact workflows follow the same path:
- Upload a file into a named bucket.
- Retrieve it later by
bucketandname. - Parse it inside a job or another platform consumer.
Uploading files
The public client import is:
from mainsequence.client import Artifact
Here is a clean version of the "vector file" upload flow:
import os
from pathlib import Path
from tqdm import tqdm
from mainsequence.client import Artifact
from mainsequence.logconf import logger
BUCKET_NAME = "Vector de precios"
upload_path = Path(os.environ["VECTOR_UPLOAD_PATH"])
vector_files = sorted(
path
for path in upload_path.iterdir()
if path.name.startswith("VectorAnalitico") and path.suffix.lower() == ".xls"
)
logger.info("Uploading %s vectors...", len(vector_files))
for path in tqdm(vector_files):
artifact = Artifact.upload_file(
filepath=str(path),
name=path.name,
bucket_name=BUCKET_NAME,
)
logger.info("Artifact available: %s (uid=%s)", path.name, artifact.uid)
Why this version is simpler
- it uses
os.environ, notos.env - it treats the upload folder as a
Path - it lets
Artifact.upload_file()handle the get-or-create behavior
The SDK implementation of upload_file() already delegates to a get_or_create endpoint, so you usually do not need a separate existence check first.
A failed upload raises the SDK's typed error with the platform's message. A
refused upload raises PermissionDeniedError, whose payload names the
required_action and the granting_policies, the policies that would allow
it; another failure raises the matching ApiError subclass, such as
BadRequestError or NotFoundError:
from mainsequence.client.exceptions import PermissionDeniedError
try:
Artifact.upload_file(filepath=str(path), name=path.name, bucket_name=BUCKET_NAME)
except PermissionDeniedError as error:
print(error.payload["required_action"], error.payload["granting_policies"])
If you want an explicit "skip if present" flow
Sometimes you still want the explicit check because you want custom logging or different behavior when the file already exists.
In that case, make sure you actually skip the upload:
existing = Artifact.filter(name=path.name, bucket__name=BUCKET_NAME)
if existing:
logger.info("Vector %s already uploaded", path.name)
continue
Without the continue, the code will log "already uploaded" and then upload anyway.
Reading an Artifact back
Once the file is in the platform, downstream code should stop depending on the original local path.
Read it back by bucket and name:
import pandas as pd
from mainsequence.client import Artifact
source_artifact = Artifact.get(
bucket__name="Vector de precios",
name="VectorAnalitico_2026_03_15.xls",
)
vector_df = pd.read_excel(source_artifact.content)
That is the important shift: the file is now referenced through the platform, not through a laptop folder.
How Artifacts fit with jobs
Artifacts and jobs work naturally together:
- a manual job can upload a new vendor file drop
- a scheduled job can refresh the bucket every day
- another platform consumer can read the Artifact
This is why Artifacts belong in the infrastructure layer of the docs. They are not just a client convenience; they are part of how files move through the platform.
Buckets and naming
Use names that will still make sense later.
Bucket names
A bucket should usually describe a domain or workflow, not a person.
Good examples:
vector_de_preciosvendor_pricesmodel_artifactsreport_exports
Artifact names
The Artifact name should identify the file clearly.
Good patterns:
VectorAnalitico_2026_03_15.xlsdaily_positions_2026_03_15.csvstress_report_2026_03_15.html
created_by_resource_name
Use a stable producer label so you can see what created the file:
vector-upload-scriptnightly-prices-jobstress-report-job
Who can use an Artifact
An Artifact's access is decided by its policies:
viewer: see the Artifact and download its file;editor: also replace its file, delete it, and see who has access;owner: also decide who has access.
Every policy that sees an Artifact also downloads it: its signed content URL
comes with each of them, and a copy someone downloaded stays with them after
the policy is removed. A replaced file reaches the people, Jobs and Agents that
read the Artifact, and a deletion cannot be undone. The person or workload that
creates an Artifact, for example with Artifact.upload_file(), holds owner;
the Organization's automation identity holds no policy on what it creates.
Organization admins hold every action.
Artifact.upload_file() with the name of an Artifact you can see replaces that
Artifact's file, which needs editor or owner: with viewer alone it raises
PermissionDeniedError with required_action artifacts.overwrite, and the
Artifact is unchanged. A name taken by an Artifact of the bucket that you cannot
see raises ConflictError. Deleting an Artifact needs editor or owner too;
a refused delete keeps the Artifact and its file.
The Jobs whose runs published an Artifact give viewer on it to everyone who
holds a policy on the Job: a run published an Artifact when the run created it
while it ran and one of the run's outputs links it. get_access() shows that as
a computed row with source inherited whose parent is the Job.
Read and change the policies with the access methods on Artifact:
from mainsequence.client import Artifact
artifact = Artifact.get(bucket__name="vendor_prices", name="prices_2026_03_15.csv")
for binding in artifact.get_access().bindings:
print(binding.uid, binding.policy.id, binding.principal.type, binding.principal.uid)
binding = artifact.attach_policy("viewer", principal_type="team", principal_uid=team_uid)
artifact.detach_policy(binding.uid)
The access methods address the Artifact by its UID alone, so they need no
Environment; with a workload's runtime credentials they reach only the
Artifacts of that workload's Environment. A refused change raises
PermissionDeniedError too. A repository workflow file's access entry for
an Artifact gives its workload the policy it names, such as policy: viewer.
mainsequence access policies --type artifacts lists the policies with their
actions; the CLI has no Artifact commands.
Who can use a bucket
A bucket's access is decided by its policies:
reader: browse its folders and the Artifacts in it that you can see;contributor: also upload Artifacts and create folders;editor: also rename the bucket, rename and delete folders, delete the bucket with every Artifact in it, and see who has access;owner: also decide who has access.
The person or workload that creates a bucket holds owner, also when
Artifact.upload_file() creates it by naming a bucket that does not exist yet,
and Organization admins hold every action. The policies go to people, workload
identities and Teams. A reader sees an Artifact in the bucket only when they
can see the Artifact itself; see
Who can use an Artifact.
Uploading into an existing bucket needs contributor or more, so give a Job or
an application that writes files into a bucket contributor on it, through its
workload identity. Without it, Artifact.upload_file() raises
PermissionDeniedError with required_action buckets.add_artifact, also when
you cannot see the bucket, because its name is taken either way.
Read and change the policies with the access methods on Bucket:
from mainsequence.client import Bucket
bucket = Bucket.get(name="vendor_prices")
for binding in bucket.get_access().bindings:
print(binding.uid, binding.policy.id, binding.principal.type, binding.principal.uid)
binding = bucket.attach_policy(
"contributor", principal_type="user", principal_uid=workload_user_uid
)
bucket.detach_policy(binding.uid)
answer = bucket.explain_access("buckets.add_artifact", principal_uid=user_uid)
print(answer.allowed, answer.granting_policies)
The access methods address the bucket by its UID alone, so they need no
Environment. A refused change raises PermissionDeniedError too.
mainsequence access policies --type buckets lists the policies with their
actions, and Who Has Access to an Object describes every
method and model. A repository workflow file's access entry for a bucket
gives its workload the policy it names, such as policy: contributor.
Common mistakes
Using the local file path as the long-term identifier
That works only on one machine. The Artifact reference is the durable identity.
Forgetting that upload_file() already uses get-or-create
If you need explicit pre-checks, do them intentionally. Otherwise, keep the upload path simple.
Logging "already uploaded" but still uploading
If you use Artifact.filter(...) first, add continue when you want to skip.
How Artifacts fit the workflow
Artifacts usually become relevant when infrastructure concepts start to matter:
- first you define the workload that produces or consumes a file
- then you learn how code runs as jobs
- then you use Artifacts to move files through the same platform
That keeps file movement aligned with the same Job and consumer lifecycle.