Skip to content

Bulk Move Sessions

fw.bulk_move_sessions() relocates many sessions into a single destination container in one request. It is the SDK's only true server-side bulk move — the rest of this section loops single-container methods, but this one operation moves a whole set in a single call. Unlike a metadata or file loop, the server resolves label conflicts for you and reports them back.

The basics of this method are introduced in Core Concepts: Copying and Moving Containers. This page is the detailed walkthrough: the conflict modes, how to preview a move, and how to read the conflict report.

The basic move

Pass a BulkMoveInput describing what to move and where. The simplest case moves a list of sessions into one subject:

Move sessions into a target subject
--8 < --"test_docs_core_concepts.py:bulk_move_sessions"

BulkMoveInput fields:

Field Type Description
sources list[str] Session IDs to move.
destinations list[str] Exactly one container ID — the single target everything moves into.
destination_container_type str The destination's type: "subjects" (normal) or "projects".
conflict_mode str How to handle label clashes: "dry", "skip", or "move".
remove_source bool Leave at the default False — see the warning below.

The call returns a list[MoveConflict]. An empty list means a clean move; a non-empty list reports the sessions that clashed at the destination.

destinations takes exactly one ID

This is "move all sources into that one container", not a parallel pairing of sources to destinations. Passing two destinations raises 422; passing zero raises a 500. Always supply a single-element list.

destination_container_type is the type of the parent container you are moving into

Although the endpoint is …/move/sessions, this field describes the parent container the sessions move into, not the sessions themselves.

Value Meaning Result
"subjects" Move sessions under a specific subject The normal case — you name the exact subject they land in.
"projects" Move sessions into a project; their parent subject is brought along See the subject behavior below.

Use "subjects" with a real subject ID for the straightforward case. A session always lives under a subject, never directly under a project, so a "projects" destination has to do something about the parent subject — covered next.

Moving into a project: what happens to the parent subject

A "projects" move ensures the session's parent subject exists in the destination project. What that means depends on whether you move all or only some of a subject's sessions:

  • Full move (all of a subject's sessions) — the same subject document (identical ID) is relocated into the destination project and removed from the source. It carries its sessions with it.

  • Partial move (only some of a subject's sessions) — a new subject (new ID, same label) is created in the destination to hold the moved sessions, while the original subject stays in the source project with its remaining sessions.

In both cases the subject's info and tags carry over to the destination subject. As with every committing move, the call returns [] — none of this is reported in the response.

Move sessions into a project destination
fw.bulk_move_sessions(
    flywheel.BulkMoveInput(
        destination_container_type="projects",
        sources=session_ids,
        destinations=[target_project_id],  # still exactly one destination
        conflict_mode="move",
        remove_source=False,
    )
)

A full move removes the subject from the source

Moving all of a subject's sessions to another project relocates the original subject (same ID) into the destination and deletes it from the source project — remove_source=False does not keep a copy behind. This operation has no "copy" mode. If the subject must remain in the source project, do not use a full "projects" move to duplicate it.

Conflict modes

A conflict is a source session whose label already exists at the destination. conflict_mode decides what happens to those:

Mode Clean session (no clash) Conflicting session Returns
"dry" reports only, moves nothing reports the conflict and the relabel it would apply, moves nothing list[MoveConflict]
"skip" moves it leaves it in place (not moved) []
"move" moves it moves it anyway, auto-relabelling the clash (e.g. "DUP 1") []

Read that as three postures: dry is a pre-flight check, skip is conservative (clean ones move, clashes stay put), and move is forceful (everything moves, names auto-resolved). Only dry ever returns a non-empty list — skip and move both resolve every case and return [].

After a committed move, the return tells you nothing

Both "skip" and "move" return [] no matter what happened — the empty list is ambiguous. After a "skip", [] can hide sessions that clashed and were silently left behind. After a "move", [] can hide sessions that were silently auto-relabelled (DUPDUP 1). A clean, conflict-free move also returns [], so you cannot distinguish "everything moved cleanly" from "something was skipped or renamed" by looking at the return.

Only "dry" returns the conflict details. If you need to know what a commit will do, run a "dry" pass first and read each MoveConflict: label is the name "move" would assign, and conflict_with is what it collides with.

Preview with a dry run

Run dry first to see conflicts without changing anything. The result is the same MoveConflict list every mode can return — for a dry run it is purely informational.

Preview a move with a dry run
--8 < --"test_docs_advanced_bulk.py:bulk_move_dry_run"

A built-in dry run is specific to this operation

The dry conflict mode is a convenience unique to bulk_move_sessions(). The other bulk patterns in this section — metadata updates, file operations — are plain client-side loops with no built-in preview, so you have to build and validate the work list yourself before writing — see Validate before you write.

Reading the conflict report

Each MoveConflict describes one clashing session and the existing container it collides with:

Field Description
id The source session being moved.
label The de-duplicated label that would be assigned (e.g. "DUP 1").
conflict_label The existing label it clashes with.
conflict_with ContainerReference ({id, type}) of the existing container.
move_type [<moving type>, <destination type>], e.g. [session, subject].
subject_code The subject code involved.
Inspect conflicts from a dry run, then force the move
--8 < --"test_docs_advanced_bulk.py:bulk_move_inspect_conflict"

The dry run's label field tells you the exact name a "move" will assign, so you can decide whether auto-relabelling is acceptable before committing:

Force the move, accepting auto-relabelling
--8 < --"test_docs_advanced_bulk.py:bulk_move_force"

Worked example: merge two subjects

A common use is consolidating one subject's sessions into another — for example after a labelling correction created a duplicate subject. Collect the source subject's session IDs and move them all into the target:

Move every session from one subject into another
--8 < --"test_docs_advanced_bulk.py:bulk_move_merge_subjects"

After the move the source subject is left empty. Delete it separately with fw.delete_subject() if you no longer need it (on audit-trail instances this requires a delete_reason — see Core Concepts: Copying and Moving Containers).

Caveats

Behavior What to do
remove_source=True always 500s, unconditionally ("Containers other than sessions are not implemented yet") Leave it False; the session still moves
destinations must hold exactly one ID Two → 422, zero → 500
Conflicts are returned, not raised Inspect the MoveConflict list; only dry returns a non-empty one
"move" mutates labels The relabel is previewed in the dry run's MoveConflict.label
A full "projects" move re-homes the whole subject The source loses the subject even with remove_source=False — see above

remove_source is not move-versus-copy

The session always relocates, even with the default remove_source=False — this operation has no copy mode. remove_source=True is unusable: it 500s across every tested combination (all conflict modes, both destination types, partial and full moves). It appears to govern post-move cleanup of the emptied source container, and that path is unimplemented on this version. Leave it False.