Skip to content

Sample Model

Field Description Settable
sample_name Human-readable name for the sample create, update
sample_type Category or type of sample (used for filtering) create, update
project_id Project this sample belongs to create; later changes use reassign_project()
project_mfid Canonical project selector for creation create
project Current project title, ID, and canonical identity when the relationship resolves server-assigned
description Free-text description of the sample create, update
timestamp Date associated with the sample (ISO 8601 format) create, update
public Whether the sample is publicly accessible (default: False) create, update
owner_orcid Canonical owner identifier returned by the API; deprecated as a creation input read; deprecated for create
owner Flexible owner identifier on create; public-safe user record on singleton reads create with an ORCID, MFID, username, or email; expanded by default on get()
unique_id System-assigned MFID identifier server-assigned
creation_time When the record was created server-assigned
modification_time When the record was last modified server-assigned
capabilities Optional caller-specific actions calculated for an exact response server-assigned

Relationships

Relationship Key(s) Description
Scientific metadata scientific_metadata in create(); metadata in update_scientific_metadata() / replace_scientific_metadata() A free-form JSON object for sample-specific properties (e.g. solubility, physical location).
Datasets dataset_mfid in link_dataset(sample_mfid, dataset_mfid) A sample can be linked to one or more datasets, and a dataset to one or more samples, capturing which material was measured.
Parent/child samples parent_mfid, child_mfid in link(); parent and child records are also accepted in create() Samples form hierarchies to represent provenance, such as boule to wafer to thin film.

Working with Samples

Creating a sample

from crucible.models import Sample

sample = client.samples.create(
    Sample(
        sample_name="Au nanoparticles batch 7",
        sample_type="nanoparticle suspension",
        project_id="my-project",
        description="5 nm Au NPs in citrate buffer, synthesized by Turkevich method",
        timestamp="2024-03-10",
    )
)

sample_mfid = sample["unique_id"]

Applications may provide project_mfid instead of project_id, or provide both when they identify the same project. Conflicting selectors produce an API validation error. The CLI keeps --project-id as its normal interactive input and exposes --project-mfid for integrations and automation.

Retrieving a sample

sample = client.samples.get("0td7evvtg5wb90005k1j97ak94")
sample_with_details = client.samples.get(
    "0td7evvtg5wb90005k1j97ak94",
    include_links=True,
    include_datasets=False,
)

Samples are retrieved only by their canonical 26-character MFID. Sample names are display values, not identifiers.

Sample detail responses retain the legacy embedded datasets collection by default for compatibility. The field is deprecated. Pass include_datasets=False to avoid loading complete dataset records, use include_links=True for lightweight relationship references, or use client.datasets.list(sample_mfid=sample_mfid) for complete paginated dataset records.

Singleton retrieval expands owner by default as a public-safe user record containing unique_id, username, first_name, and last_name. Pass include_owner=False to suppress expansion. List operations remain opt-in with include_owner=True. The canonical owner identifier remains available as owner_orcid.

Canonical detail responses include caller-specific capabilities when the server has calculated them. Sample capabilities always report can_change_status=False because samples have no lifecycle-status operation. Collections and search results normally return capabilities=None, which means the guidance was not calculated rather than that every action is denied. The API remains authoritative for each mutation.

Sample responses include a lightweight project reference when the canonical relationship resolves. Use its title and project_id for display and its unique_id for stable navigation. The flat project_id remains the compatibility fallback for legacy records. A project reference does not imply permission to retrieve the complete project.

Listing samples

# All samples in a project
samples = client.samples.list(project_id="my-project", limit=50)

# Samples shared with a project but assigned elsewhere or unassigned
shared = client.samples.list(project_id="my-project", project_scope="shared")

# Assigned and shared samples using the project's canonical MFID
visible = client.samples.list(
    project_mfid="0tkn2knjast3h0008nyq9zps2c",
    project_scope="all",
)

# Samples linked to a specific dataset
samples = client.samples.list(dataset_mfid="0tkn2knjast3h0008nyq9zps2c")

# Samples readable by a user and directly accessible to a project
samples = client.samples.list(
    accessible_to_user="alice",
    accessible_to_project="my-project",
)

Pass the project slug as project_id or the canonical project MFID as project_mfid. Both may be supplied when they identify the same project; conflicting identifiers produce an API validation error. project_scope accepts assigned, shared, or all and defaults to assigned. Scoped collection results expose project_relation as assigned or shared; project may be None when a shared resource has no primary project.

The dataset_mfid relationship filter uses the normal paginated sample collection and can be combined with compatible sample and access filters. Results follow cursor pagination and include only samples the caller may read.

Multiple user and project access selectors use intersection semantics and never broaden what the authenticated caller may read.

Updating a sample

client.samples.update(
    "0td7evvtg5wb90005k1j97ak94",
    description="5 nm Au NPs, annealed at 200 C for 2h after synthesis",
)

Project and ownership changes use preview-first workflows:

project_preview = client.samples.reassign_project(sample_mfid, "new-project")
client.samples.reassign_project(sample_mfid, "new-project", confirm=True)
owner_preview = client.samples.transfer_ownership(sample_mfid, "new-owner@example.org")
client.samples.transfer_ownership(sample_mfid, "new-owner@example.org", confirm=True)

Managing access

grants = client.samples.list_access(sample_mfid)
client.samples.set_access(sample_mfid, "users", "0000-0002-1825-0097", "viewer")
client.samples.set_public(sample_mfid)
client.samples.set_private(sample_mfid)

Normal access grants accept viewer, contributor, editor, or admin. Use transfer_ownership() for ownership.

Sample hierarchies

Samples can form parent-child trees to represent provenance. Use link() to connect an existing parent to a child:

# Link a wafer (child) to the boule it was cut from (parent)
client.samples.link(
    parent_mfid=boule_sample_mfid,
    child_mfid=wafer_sample_mfid,
)

You can also pass parents or children lists at creation time:

thin_film = client.samples.create(
    Sample(
        sample_name="TiO2 thin film on Si",
        sample_type="thin film",
        project_id="my-project",
    ),
    parents=[{"unique_id": wafer_sample_mfid}],
)

Navigate the hierarchy:

parents = client.samples.list_parents(sample_mfid)
children = client.samples.list_children(sample_mfid)

Linking samples to datasets

# Link a dataset to a sample
client.samples.link_dataset(sample_mfid=sample_mfid, dataset_mfid=dataset_mfid)

# Remove the link
client.samples.unlink_dataset(sample_mfid=sample_mfid, dataset_mfid=dataset_mfid)

Viewing the sample graph

# First-degree connections (datasets, parent/child samples)
graph = client.samples.graph(sample_mfid)

# Full connected component
graph = client.samples.graph(sample_mfid, recursive=True)

# As a networkx DiGraph (requires networkx)
import networkx as nx
G = client.samples.graph(sample_mfid, recursive=True, as_networkx=True)