SOKO Actions Developer Documentation (Draft v1.2)
Scope: this document is inferred from the provided code and explicitly marks TODOs where the contract is not fully observable from code alone.
Update v1.2: added missing “contract-critical” details discovered from your in-house action pattern (notably SokoJob init signature, required InitApi() call, and canonical component path resolution).Convert
Table of contents
Action discovery & inventory SOKO Action specification (contract) Base infrastructure: SokoJob Action reference (per-action chapters) SetShotFrameRangeFromVersion How to build a new Action Gap analysis, risks, and recommendations 1) Action discovery & inventory
Execution host / runner
soko_electon_action_runner_example.js – Electron runner that writes script.py + arguments.json into a temp dir and executes Python via PythonShell with -u (unbuffered) and captures stdout lines as messages. Base class
SokoJob.py – reads the JSON args file and creates an authenticated soko_api client. Important clarification (v1.2):
While the runner passes one positional CLI argument (the arguments.json path), your action code convention passes sys.argv (the full argv list) into SokoJob, not only the JSON path. This affects how new actions must be written.Convert
Discovered Actions (entrypoints + files)
All actions follow the same pattern: define a class inheriting SokoJob, override Handle(), and run it under if __name__ == "__main__":.
Discovered Action input-form schema files
SetShotFrameRangeFromVersion.json – defines a startFrame input with default 1001. Format appears non-standard JSON Schema (see TODO in spec). 2) SOKO Action specification (contract)
2.1 Execution model (confirmed)
Where actions run: locally, on the user machine via Electron runner.
Invocation steps (confirmed):
Runner creates a temp directory and writes script.py and arguments.json. Runner executes Python with -u and passes a single argument: the JSON path (args: [argumentsFile]). Runner listens to each stdout line and classifies the line as WARNING/ERROR/INFO by substring match. Critical clarification (v1.2): argv contract for action authors
sys.argv[0] = <script.py path> sys.argv[1] = <arguments.json path> But your in-house action pattern passes the entire sys.argv list into the action class, and SokoJob consumes that (internally) to locate the json file.Convert Canonical entrypoint pattern (MUST for compatibility):
if __name__ == "__main__":
plugin = MyAction(sys.argv)
plugin.Handle()
…and constructor:
class MyAction(SokoJob):
def __init__(self, args):
super().__init__(args)
Convert
Concurrency (confirmed):
Runner maintains a task queue and can run actions in parallel up to MAX_CONCURRENT_ACTION_PROCESSES. If multiple actions are submitted in one payload, they run sequentially. Timeouts / retries (TODO):
No explicit timeout or retry policy is visible in the provided runner sample. TODO: where/if SOKO defines action timeouts, max runtime, retry on non-zero exit, and how failureDetectionJobErrors is used (several actions set it to 3). 2.2 Inputs: arguments.json schema (confirmed + inferred)
Top-level structure (confirmed):
The runner writes a JSON object containing at least:
contextData (user, project, folder) contextObjects (the selected objects) Required fields (confirmed by base class / runner):
pluginData.serverURL, pluginData.apiKey are required to init soko_api. pluginData.apiUser + pluginData.apiUserPassword, or pluginData.userApiKey, or pluginData.jwt
must exist, else login fails. contextObjects must exist for most actions (they iterate self.arguments["contextObjects"]). 2.2.1 contextType="version" – minimum reliable schema (v1.2, PARTIALLY CONFIRMED)
Based on observed actions, a Version context object typically contains:
components[].id (component UUID) components[].parsedPaths (list of OS-specific path entries) Optional / may exist, but not guaranteed (TODO to confirm):
name and/or output.name (human-readable) publishedAt / published_at publishedBy / published_by output.* (folderId, parentId, outputTypeId, suffix) depending on action Recommendation (MUST for action authors): treat anything beyond id, versionNumber, components[].parsedPaths as optional unless explicitly stated for the action.
2.2.2 Component path resolution (v1.2, CONFIRMED)
Earlier draft referenced get_component_path_for_current_os(), but your codebase convention uses:
from soko_api.utils import files
path_data = files.get_component_path_data_for_current_os(component["parsedPaths"])
local_path = path_data["path"]
Where path_data appears to be a dict (at least containing path, and possibly also prefix / prefixVariable).Convert
This is the canonical method (MUST) to resolve local file paths.
Do not hand-pick parsedPaths entries unless you have a hard reason.
2.2.3 Sequences / printf patterns (PARTIALLY CONFIRMED + TODO)
Some actions support sequences via % patterns (e.g. shot.%04d.exr). Some actions explicitly reject sequences (UploadToSoko). TODO (minimal data):
Confirm whether soko_api.utils.files provides a canonical helper like uncompress_sequence() / get_sequences() for expanding a %0Nd pattern into a file list, and standardize on it. If no helper exists, implement a local expansion utility (directory scan + regex). 2.3 Output / feedback contract (confirmed)
There is no structured JSON return from Python to the runner today; the runner treats stdout lines as logs.
Success: Python exits normally (runner sends status 200). Failure: raise/throw → non-zero exit / PythonShell error → runner sends status 500. Important runner bug (confirmed):
Runner captures an exception variable when a message contains “ERROR”, but then always returns exception: null on the success response. So “ERROR” log lines alone do not propagate failure to UI—only process failure does. Implication for Action authors (MUST):
If an action must fail, raise an exception (do not rely on logging “ERROR”). 2.4 Side-effects & external IO (confirmed)
Actions commonly:
Read local files (including sequences via % patterns). Call SOKO backend via soko_api.table(...).insert/update or soko_api.rpc(...). Spawn external binaries (ffmpeg, ffprobe). Call external services (Dropbox SDK). 2.5 Configuration & secrets (confirmed + security note)
Runner injects pluginData from SOKO settings keys like: SOKO_SERVER_URL, SOKO_API_KEY, SOKO_USER_JWT_TOKEN, SOKO_USER_API_KEY refresh token read from settings table key DROPBOX_REFRESH_TOKEN app key from arguments["settings"]["DROPBOX_APP_KEY"] Critical: temp dir cleanup is currently disabled (confirmed)
Runner creates temp dir with keep: true and does not call cleanupCallback() (commented out). This means JWT/API keys may remain on disk. 2.6 Observability & logging (confirmed + recommended rules)
Current behavior:
Python logging is configured as '%(levelname)s: %(message)s' to stdout. Runner decides log level by substring match on the emitted line. Action author guidelines (MUST):
Use logging.info/warning/error/exception (not print) so messages include INFO: / WARNING: / ERROR: prefix. Avoid putting the word ERROR inside non-error messages, because the runner will treat the line as an error. Recommended extension (COULD): structured final result
Emit exactly one line at the end: Teach runner to parse it (safe, backwards compatible). (Runner changes are TODO.)
3) Base infrastructure: SokoJob
What SokoJob does (confirmed + clarified v1.2)
Initialization / argv handling (CONFIRMED): Your action classes pass sys.argv into SokoJob via super().__init__(args) and this is the canonical pattern.Convert Internally, SokoJob loads arguments JSON from the last CLI arg (effectively sys.argv[-1]). Configures Python logging to stdout. Prepares an authenticated self.soko_api client using Supabase-like create_client(url, key) and signs in via one of: password, API key, or JWT. Important lifecycle requirement (v1.2, CONFIRMED): Many actions must call self.InitApi() inside Handle() before using self.soko_api. This is the observed pattern in your code.Convert
4) Action reference (per-action)
Each chapter uses the same template.
4.1 UploadToSoko (script.py)
Summary
Uploads a single published component file to SOKO “Media Cloud” and marks it isReviewable=True. Sequences (%) are explicitly rejected. Entrypoint
UploadToSoko.Handle() contextObjects[].components[].parsedPaths (required) Outputs
SOKO upload: self.soko_api.upload_file("fileComponents", path, component_id) DB update: components.isReviewable=True Flow
For each selected version → for each component: resolve local path (canonical resolver: files.get_component_path_data_for_current_os(...)) sequenceDiagram
participant User
participant Runner
participant Action as UploadToSoko
participant Soko as SOKO API
User->>Runner: Run action on Version
Runner->>Action: python -u script.py arguments.json
Action->>Soko: upload_file(fileComponents, local_path, component_id)
Action->>Soko: update components set isReviewable=true
Action-->>Runner: exit 0
soko_api client library (bundled in SOKO Python env). Sequences not supported → hard failure. Upload size dominates. Consider chunking at SDK level (not visible here). Validate resolved local paths are under allowed roots (TODO: central helper). Example payload
See Example Payload A (below). 4.2 ConvertAndUploadToSoko (ConvertAndUploadToSoko.py)
Summary
Converts a component to MP4 (if not already converted) using soko_api.utils.video.convert(...), uploads it, and marks isReviewable=True. Reads FPS from folder custom attribute fps (default 25). Entrypoint
ConvertAndUploadToSoko.Handle() contextObjects[].components[].parsedPaths