Skip to content
helmstudio
Contents

timeline

GET /timeline

The sequences the caller may read, newest first.

Q11. The caller's own, and every studio's when it holds `gallery.read_all`. Deleted sequences are not listed.

Needs the timeline capability.

GoList(ctx context.Context, params *TimelineListParams) (*TimelinePage, error)
Pythondef list(self, *, limit: Optional[Any] = None, cursor: Optional[Any] = None) -> Any
JavaScriptlist(params = {})
ParameterInTypeAbout
limitqueryinteger
cursorquerystringThe `next_cursor` of the previous page, unchanged.
StatusBodyMeans
200TimelinePageA page of sequences.
400ErrorThe request is malformed. Codes: `bad_request`, `bad_filter`, `bad_cursor`, `invalid_document`.
401ErrorNo studio token, or a revoked or unknown one. Code `unauthenticated`.
403ErrorThe token lacks a capability (`capability_required`, with `details.capability`), the job is not a task the studio may change (`not_a_task`), or the request came from another origin (`bad_origin`).

POST /timeline

Create a sequence from clips, or from whole tracks.

R44, R45, 05 §7. Either `clips`, which are laid end to end from 0 on V1 for video and images and on A1 for sound, or `tracks` — not both. Neither makes an empty sequence, which is valid to store and cannot be exported until it has a video clip. `target.sample_rate` defaults to 48000 (Q9). Every clip must name an asset the caller may read (Q11); one it may not is 422 `invalid_timeline`, worded the same whether or not the asset exists.

Needs the timeline capability.

GoCreate(ctx context.Context, body TimelineCreate) (*Timeline, error)
Pythondef create(self, body: Dict[str, Any]) -> Any
JavaScriptcreate(body)

Request body: TimelineCreate (application/json)

StatusBodyMeans
201TimelineThe new sequence, as it was stored after snapping.
400ErrorThe request is malformed. Codes: `bad_request`, `bad_filter`, `bad_cursor`, `invalid_document`.
401ErrorNo studio token, or a revoked or unknown one. Code `unauthenticated`.
403ErrorThe token lacks a capability (`capability_required`, with `details.capability`), the job is not a task the studio may change (`not_a_task`), or the request came from another origin (`bad_origin`).
422ErrorWell-formed but refused. Codes: `outside_roots`, `cross_device`, `not_a_file`, `unknown_session`, `self_handoff`, `not_launchable`, `blocked`, `not_linkable`.

GET /timeline/{id}

One sequence. One the caller may not read is 404.

Needs the timeline capability.

GoGet(ctx context.Context, id string) (*Timeline, error)
Pythondef get(self, id: str) -> Any
JavaScriptget(id)
ParameterInTypeAbout
idpath · requiredULID
StatusBodyMeans
200TimelineThe sequence. `ETag` is its revision.
401ErrorNo studio token, or a revoked or unknown one. Code `unauthenticated`.
403ErrorThe token lacks a capability (`capability_required`, with `details.capability`), the job is not a task the studio may change (`not_a_task`), or the request came from another origin (`bad_origin`).
404ErrorAbsent, deleted, or not the caller's to see. Code `not_found`.

PATCH /timeline/{id}

Edit a sequence. Every edit is a revision.

05 §6, Q10. A JSON merge patch over `name`, `target` and `tracks`; `target` members merge, and `tracks` replaces. `If-Match` is required, and a stale one is 409 `etag_mismatch`. The document the patch produces is validated and snapped as a whole, and the document it replaces is kept as the previous revision. A clip this write adds must name an asset the caller may read; one already in the sequence may stay (Q11).

Needs the timeline capability.

GoUpdate(ctx context.Context, id string, body map[string]any, params *TimelineUpdateParams) (*Timeline, error)
Pythondef update(self, id: str, body: Dict[str, Any], *, if_match: Any) -> Any
JavaScriptupdate(id, body, params = {})
ParameterInTypeAbout
idpath · requiredULID
If-Matchheader · requiredstringThe revision last read, as the `ETag` header gave it. Required on a timeline write, because a merge patch replaces `tracks` whole and a blind write would drop another editor's clips (docs/decisions.md M8 Q10). A stale one is 409 `etag_mismatch`.

Request body: TimelineUpdate (application/merge-patch+json)

StatusBodyMeans
200TimelineThe sequence as it was stored.
400ErrorThe request is malformed. Codes: `bad_request`, `bad_filter`, `bad_cursor`, `invalid_document`.
401ErrorNo studio token, or a revoked or unknown one. Code `unauthenticated`.
403ErrorThe token lacks a capability (`capability_required`, with `details.capability`), the job is not a task the studio may change (`not_a_task`), or the request came from another origin (`bad_origin`).
404ErrorAbsent, deleted, or not the caller's to see. Code `not_found`.
409ErrorCodes: `etag_mismatch`, `name_taken`, `job_finished`, `already_running`, `not_running`, `port_conflict`, `heavy_conflict` (with `details.heavy`), `not_installed`, `conflict`, `in_use`, `missing`, `confirm_required` and `preview_changed` (with `details.preview`), `not_reclaimable`, and from M7: `approval_required` (with `details.approval`), `id_taken`, `not_fetched`, `already_exists`.
422ErrorWell-formed but refused. Codes: `outside_roots`, `cross_device`, `not_a_file`, `unknown_session`, `self_handoff`, `not_launchable`, `blocked`, `not_linkable`.

DELETE /timeline/{id}

Delete a sequence. Its exports and their lineage stay.

Q12. A soft delete: the row stays, so an exported item keeps naming it, and its clips stop holding their footage against reclaim. There is no restore. `If-Match` is checked when sent.

Needs the timeline capability.

GoDelete(ctx context.Context, id string, params *TimelineDeleteParams) error
Pythondef delete(self, id: str, *, if_match: Optional[Any] = None) -> Any
JavaScriptdelete(id, params = {})
ParameterInTypeAbout
idpath · requiredULID
If-MatchheaderstringThe etag last read. When given and stale, the write is refused with 409 `etag_mismatch`.
StatusBodyMeans
204Deleted.
401ErrorNo studio token, or a revoked or unknown one. Code `unauthenticated`.
403ErrorThe token lacks a capability (`capability_required`, with `details.capability`), the job is not a task the studio may change (`not_a_task`), or the request came from another origin (`bad_origin`).
404ErrorAbsent, deleted, or not the caller's to see. Code `not_found`.
409ErrorCodes: `etag_mismatch`, `name_taken`, `job_finished`, `already_running`, `not_running`, `port_conflict`, `heavy_conflict` (with `details.heavy`), `not_installed`, `conflict`, `in_use`, `missing`, `confirm_required` and `preview_changed` (with `details.preview`), `not_reclaimable`, and from M7: `approval_required` (with `details.approval`), `id_taken`, `not_fetched`, `already_exists`.

GET /timeline/{id}/exports

The sequence's export jobs, newest first.

Q17. Under `timeline`, so a studio that exports need not also hold `jobs` to watch what it started. The same jobs appear in `/jobs` for a studio that does hold it.

Needs the timeline capability.

GoExports(ctx context.Context, id string, params *TimelineExportsParams) (*JobPage, error)
Pythondef exports(self, id: str, *, limit: Optional[Any] = None, cursor: Optional[Any] = None) -> Any
JavaScriptexports(id, params = {})
ParameterInTypeAbout
idpath · requiredULID
limitqueryinteger
cursorquerystringThe `next_cursor` of the previous page, unchanged.
StatusBodyMeans
200JobPageA page of export jobs.
400ErrorThe request is malformed. Codes: `bad_request`, `bad_filter`, `bad_cursor`, `invalid_document`.
401ErrorNo studio token, or a revoked or unknown one. Code `unauthenticated`.
403ErrorThe token lacks a capability (`capability_required`, with `details.capability`), the job is not a task the studio may change (`not_a_task`), or the request came from another origin (`bad_origin`).
404ErrorAbsent, deleted, or not the caller's to see. Code `not_found`.

POST /timeline/{id}/exports/{job}:cancel

Stop a running export and leave nothing behind.

Q16, Q17. The ffmpeg leading the export's process group is stopped and its partial file removed; the job ends `cancelled`. A finished job is 409 `job_finished`. The launcher's `POST /launcher/jobs/{id}:cancel` reaches the same runner.

Needs the timeline capability.

GoCancelExport(ctx context.Context, id string, job string) error
Pythondef cancel_export(self, id: str, job: str) -> Any
JavaScriptcancelExport(id, job)
ParameterInTypeAbout
idpath · requiredULID
jobpath · requiredULID
StatusBodyMeans
204Cancellation requested.
401ErrorNo studio token, or a revoked or unknown one. Code `unauthenticated`.
403ErrorThe token lacks a capability (`capability_required`, with `details.capability`), the job is not a task the studio may change (`not_a_task`), or the request came from another origin (`bad_origin`).
404ErrorAbsent, deleted, or not the caller's to see. Code `not_found`.
409ErrorCodes: `etag_mismatch`, `name_taken`, `job_finished`, `already_running`, `not_running`, `port_conflict`, `heavy_conflict` (with `details.heavy`), `not_installed`, `conflict`, `in_use`, `missing`, `confirm_required` and `preview_changed` (with `details.preview`), `not_reclaimable`, and from M7: `approval_required` (with `details.approval`), `id_taken`, `not_fetched`, `already_exists`.

GET /timeline/{id}/revisions

The sequence's earlier revisions, newest first.

Q10. Each write stores the document it replaced; the newest 100 are kept. Undo is `:revert`.

Needs the timeline capability.

GoRevisions(ctx context.Context, id string, params *TimelineRevisionsParams) (*TimelineRevisionPage, error)
Pythondef revisions(self, id: str, *, limit: Optional[Any] = None, cursor: Optional[Any] = None) -> Any
JavaScriptrevisions(id, params = {})
ParameterInTypeAbout
idpath · requiredULID
limitqueryinteger
cursorquerystringThe `next_cursor` of the previous page, unchanged.
StatusBodyMeans
200TimelineRevisionPageA page of revisions.
400ErrorThe request is malformed. Codes: `bad_request`, `bad_filter`, `bad_cursor`, `invalid_document`.
401ErrorNo studio token, or a revoked or unknown one. Code `unauthenticated`.
403ErrorThe token lacks a capability (`capability_required`, with `details.capability`), the job is not a task the studio may change (`not_a_task`), or the request came from another origin (`bad_origin`).
404ErrorAbsent, deleted, or not the caller's to see. Code `not_found`.

POST /timeline/{id}:export

Export the sequence. Answers with the job that renders it.

R46, R48, Q16, Q17. Export is a Job, so progress, cancellation and the log are the ones that already exist. The output is written where nothing looks for it and becomes an asset only after ffmpeg exits 0 and the file is probed against the target; then it is adopted, and recorded as a gallery item carrying `timeline_id` with one `clip` input per distinct asset. A sequence with no video clip is 422 `invalid_timeline`. A clip the caller may no longer read is 422. One export at a time per sequence: another is 409 `already_exporting`. Needs ffmpeg (501 `unsupported`).

Needs the timeline capability.

GoExport(ctx context.Context, id string, body ExportRequest) (*Job, error)
Pythondef export(self, id: str, body: Dict[str, Any]) -> Any
JavaScriptexport(id, body)
ParameterInTypeAbout
idpath · requiredULID

Request body: ExportRequest (application/json)

StatusBodyMeans
202JobThe export job.
400ErrorThe request is malformed. Codes: `bad_request`, `bad_filter`, `bad_cursor`, `invalid_document`.
401ErrorNo studio token, or a revoked or unknown one. Code `unauthenticated`.
403ErrorThe token lacks a capability (`capability_required`, with `details.capability`), the job is not a task the studio may change (`not_a_task`), or the request came from another origin (`bad_origin`).
404ErrorAbsent, deleted, or not the caller's to see. Code `not_found`.
409ErrorCodes: `etag_mismatch`, `name_taken`, `job_finished`, `already_running`, `not_running`, `port_conflict`, `heavy_conflict` (with `details.heavy`), `not_installed`, `conflict`, `in_use`, `missing`, `confirm_required` and `preview_changed` (with `details.preview`), `not_reclaimable`, and from M7: `approval_required` (with `details.approval`), `id_taken`, `not_fetched`, `already_exists`.
422ErrorWell-formed but refused. Codes: `outside_roots`, `cross_device`, `not_a_file`, `unknown_session`, `self_handoff`, `not_launchable`, `blocked`, `not_linkable`.
501ErrorThis provider cannot serve this. Code `unsupported`, with a reason in `message`.

POST /timeline/{id}:open

Ask the framework to show its editor on this sequence.

R45, 05 §5a, §7, Q19. The data operation always works and only the presentation degrades: until the launcher has a Timeline screen, which waits for M9's cookie (Q20), this answers 501 `unsupported` with a reason, and a studio hides its button. From then it publishes the sequence to a connected launcher page and answers `{opened, surface}`, or 501 when no page is connected.

Needs the timeline capability.

GoOpen(ctx context.Context, id string) (*TimelineOpened, error)
Pythondef open(self, id: str) -> Any
JavaScriptopen(id)
ParameterInTypeAbout
idpath · requiredULID
StatusBodyMeans
200TimelineOpenedA framework surface opened the sequence.
401ErrorNo studio token, or a revoked or unknown one. Code `unauthenticated`.
403ErrorThe token lacks a capability (`capability_required`, with `details.capability`), the job is not a task the studio may change (`not_a_task`), or the request came from another origin (`bad_origin`).
404ErrorAbsent, deleted, or not the caller's to see. Code `not_found`.
501ErrorThis provider cannot serve this. Code `unsupported`, with a reason in `message`.

GET /timeline/{id}:plan

Which path an export would take, and why.

R47, Q13. Every video clip is probed, and the answer is `copy` when every one of them matches the target and each other — codec, profile, level, size, aspect ratio, field order, pixel format, time base, frame rate, colour tags and parameter sets — and is used whole, with no hold, image or transition. Otherwise `conform`, with a reason per clip. The picture is copied; sound always goes through the conform graph, because a copied AAC stream carries its own priming and padding into every cut. So the chip reads "video stream copy", not "no re-encode". Needs ffmpeg: without it, 501 `unsupported` with `details.tool`.

Needs the timeline capability.

GoPlan(ctx context.Context, id string, params *TimelinePlanParams) (*ExportPlan, error)
Pythondef plan(self, id: str, *, preset: Optional[Any] = None) -> Any
JavaScriptplan(id, params = {})
ParameterInTypeAbout
idpath · requiredULID
presetqueryExportPreset
StatusBodyMeans
200ExportPlanThe plan.
400ErrorThe request is malformed. Codes: `bad_request`, `bad_filter`, `bad_cursor`, `invalid_document`.
401ErrorNo studio token, or a revoked or unknown one. Code `unauthenticated`.
403ErrorThe token lacks a capability (`capability_required`, with `details.capability`), the job is not a task the studio may change (`not_a_task`), or the request came from another origin (`bad_origin`).
404ErrorAbsent, deleted, or not the caller's to see. Code `not_found`.
422ErrorWell-formed but refused. Codes: `outside_roots`, `cross_device`, `not_a_file`, `unknown_session`, `self_handoff`, `not_launchable`, `blocked`, `not_linkable`.
501ErrorThis provider cannot serve this. Code `unsupported`, with a reason in `message`.

POST /timeline/{id}:revert

Write an earlier revision back as the newest one.

Q10. Undo is the previous revision, written forward rather than rewound, so nothing is lost. A revision naming an asset that has since been reclaimed, or one the caller may not read, is refused with the clips named (422 `invalid_timeline`). `If-Match` is required.

Needs the timeline capability.

GoRevert(ctx context.Context, id string, body TimelineRevert, params *TimelineRevertParams) (*Timeline, error)
Pythondef revert(self, id: str, body: Dict[str, Any], *, if_match: Any) -> Any
JavaScriptrevert(id, body, params = {})
ParameterInTypeAbout
idpath · requiredULID
If-Matchheader · requiredstringThe revision last read, as the `ETag` header gave it. Required on a timeline write, because a merge patch replaces `tracks` whole and a blind write would drop another editor's clips (docs/decisions.md M8 Q10). A stale one is 409 `etag_mismatch`.

Request body: TimelineRevert (application/json)

StatusBodyMeans
200TimelineThe sequence, at its new revision.
400ErrorThe request is malformed. Codes: `bad_request`, `bad_filter`, `bad_cursor`, `invalid_document`.
401ErrorNo studio token, or a revoked or unknown one. Code `unauthenticated`.
403ErrorThe token lacks a capability (`capability_required`, with `details.capability`), the job is not a task the studio may change (`not_a_task`), or the request came from another origin (`bad_origin`).
404ErrorAbsent, deleted, or not the caller's to see. Code `not_found`.
409ErrorCodes: `etag_mismatch`, `name_taken`, `job_finished`, `already_running`, `not_running`, `port_conflict`, `heavy_conflict` (with `details.heavy`), `not_installed`, `conflict`, `in_use`, `missing`, `confirm_required` and `preview_changed` (with `details.preview`), `not_reclaimable`, and from M7: `approval_required` (with `details.approval`), `id_taken`, `not_fetched`, `already_exists`.
422ErrorWell-formed but refused. Codes: `outside_roots`, `cross_device`, `not_a_file`, `unknown_session`, `self_handoff`, `not_launchable`, `blocked`, `not_linkable`.

POST /timeline:append

Add one asset to the end of a track, without stealing focus.

R45, Q19. With `timeline_id`, that sequence; without one, the most recently updated sequence the caller owns, and 409 `no_timeline` when it owns none. `track` names a track by kind and position — `V1`, `A1`, `A2` — and defaults to `V1` for a video or image asset and `A1` for sound. It applies to whatever the current revision is, so it needs no `If-Match`.

Needs the timeline capability.

GoAppend(ctx context.Context, body TimelineAppend) (*Timeline, error)
Pythondef append(self, body: Dict[str, Any]) -> Any
JavaScriptappend(body)

Request body: TimelineAppend (application/json)

StatusBodyMeans
200TimelineThe sequence, with the clip appended.
400ErrorThe request is malformed. Codes: `bad_request`, `bad_filter`, `bad_cursor`, `invalid_document`.
401ErrorNo studio token, or a revoked or unknown one. Code `unauthenticated`.
403ErrorThe token lacks a capability (`capability_required`, with `details.capability`), the job is not a task the studio may change (`not_a_task`), or the request came from another origin (`bad_origin`).
404ErrorAbsent, deleted, or not the caller's to see. Code `not_found`.
409ErrorCodes: `etag_mismatch`, `name_taken`, `job_finished`, `already_running`, `not_running`, `port_conflict`, `heavy_conflict` (with `details.heavy`), `not_installed`, `conflict`, `in_use`, `missing`, `confirm_required` and `preview_changed` (with `details.preview`), `not_reclaimable`, and from M7: `approval_required` (with `details.approval`), `id_taken`, `not_fetched`, `already_exists`.
422ErrorWell-formed but refused. Codes: `outside_roots`, `cross_device`, `not_a_file`, `unknown_session`, `self_handoff`, `not_launchable`, `blocked`, `not_linkable`.