Reference
Run results, account security and subscription sign-in.
What a finished run records and how it is read, how Weles signs in to a subscription for Brama, and where account security is described.
Account security#
The account-security capability is described at /docs/account-security. It requires a matching client, API action catalog and managed worker revision. A queued request is not a 2FA verdict, and a stored authenticator seed is not the provider setting.
Subscription authentication#
Brama resolves provider and subscription_id with POST /reauth/resolve, then submits the returned account_revision to POST /reauth. Weles uses the matching Skarbiec login on its Stado-selected host and persists the OAuth grant to that same subscription. A successful browser login alone is not repair: Brama must refresh the stored credential successfully.
A subscription item that records no account identity is resolved from the member id Brama's pool minted for it. `brama subscription sync` names each held account `brama-sub-held-<provider>-<account-slug>`, where the slug is the account lowercased with every non-alphanumeric character turned into a hyphen. Weles matches that forward: every candidate login's own account_ref is slugged the same way and compared with the id's suffix, and the identity is accepted only when exactly one login matches. Nothing is derived from the slug itself. A suffix matching no login, or more than one, still fails with subscription_identity_missing, which is what every member imported before accounts were recorded beside their grants used to return — and why the automatic sign-in that exists to replace their burnt grants could not start at all.
How Weles signs into the chosen login row is that row's login_method: google_sso or email_password. A row's sealed context declares it; a row imported without one may carry the tag weles:login-method:<method> instead, which the vault owner writes with stado credentials item retag --host <vault owner> <login item> --tags <the row's tags>,weles:login-method:google_sso (Stado Desktop: Hosts → Credentials → Available operations… → Read or change item tags). A row that declares neither is refused with login_method_unsupported, and the refusal prints that exact command with the row's current tags kept, so declaring the method does not drop the tags Brama selects the row by. Every field a row may declare in more than one place is read from all of them: a login_method in the context and a different one in the tag, a login_item in the context, the credential metadata's login_item or its legacy <PROVIDER>_SERVICE_CREDENTIAL_ID that differ, or an account_ref in the context that differs from the row's username other than by letter case, is refused with login_record_conflict naming each place and what it says, so keep one. A credential value or credential metadata that is not JSON is refused with login_record_unreadable naming the row, never read as a row without metadata. node tests/identity/login-records.mjs runs these refusals against a vault of its own after Weles is built.
Google may offer Enter your password instead of showing a password field. Weles selects that visible method before Try another way. It does not treat a hidden Too many failed attempts template as a refusal, and it does not repeatedly submit the selected challenge. Codex and Claude credential entry observe the enabled Next button after field validation, and then the visible password field, method or refusal, without an idle gap before those conditions. The Codex Google account chooser likewise observes its enabled Continue button directly. These conditions authorize the next input; they do not by themselves prove authentication.
A visible Google refusal returns BROWSER_NOT_SECURE or provider_challenge_refused. If the chosen password field never appears, google_password_challenge_unavailable records the observed host, path and visible text. These failures retain the actual stage and are not automatically retried against unchanged identity and software.
Get a code to sign in is an unfinished Google verification step, even without a 2-Step Verification heading. Weles recognizes the challenge URL and visible page, and waits for a submitted code to leave that challenge before continuing. The code is computed only from the selected Skarbiec login's authenticator seed (totp_secret); no code typed by a person or set in the environment is read. Weles picks the method whose accessible name is Google Authenticator, and when Google shows another method first it opens Google's own Try another way chooser and picks it there; a chooser that lists no authenticator method ends as stuck. google_2fa_material_missing means the selected Skarbiec login has no authenticator seed; no alternative that sends a notification or SMS is selected. google_sign_in_state_unavailable means navigation prevented an observed state, not that authentication succeeded. Stado credentials seed-freshness reports stored seed availability separately from successful login evidence. A login with an empty seed is enrolled by the google/authenticator/enrol trajectory (WELES_LOGIN_ITEM names the Skarbiec login): it signs in with the stored password, waits once for the operator to approve Google's prompt on the phone, opens the account's authenticator setup, reads the setup key Google shows behind Can't scan it?, confirms the first code, and writes the key to the login item's totp_secret, read back before it reports ok. Every stop is named in its result: google_push_not_approved, google_sign_in_required, authenticator_setup_page_not_reached, setup_action_not_found, setup_key_reveal_not_found, setup_key_not_shown, setup_code_input_not_found, setup_code_submit_not_found, setup_code_rejected, seed_persist_unconfirmed; a page preview in a refusal has the key redacted.
Read GET /diagnostics/<run_id> and its run-result.json entry for the recorded result, source_revision and retained DOM, network and video files. Session closure stops its measurement timers; a recorded failure must not be converted into a later worker timeout.
Purchased subscription identity#
For a subscription acquisition run, the request's subscription_id identifies the request, not the permanent subscription. The acquisition trajectory supplies the provider name and the email address used to register the purchased account. Weles derives the permanent subscription_id from that provider name and email address.
Before attempting OAuth sign-in, Weles stores the account email, plan and login method in Skarbiec and creates a separate subscription record referencing that login. The run returns subscription_id, account, login_item and subscription_item. If either record already exists, acquisition refuses to overwrite it. A sign-in failure leaves the stored account available for another sign-in attempt.
Brama signs in that exact account through Weles to obtain its own OAuth grant; it does not copy another application's tokens. Brama checks the returned account and subscription_id before using the grant. Names such as primary do not give a subscription preference when selecting an available member to serve a model request.
Registration account persistence#
On the Stado-selected dedicated host, node src/trajectories/producthunt/register.mjs and node src/trajectories/meta/threads_register.mjs register through existing X and Instagram accounts respectively. State PROXY_URL explicitly. The source account must carry its actual email in metadata, password in the Skarbiec credential field and authenticated cookies. twitter_account_incomplete or instagram_account_incomplete names a missing source identity before provider account creation; no address or linked-login password is invented. Product Hunt also requires the handle read from its authenticated page and refuses ph_handle_unreadable instead of saving under the X handle.
WSession.saveAccount writes the platform credential row to Skarbiec and throws on a failed item write, so callers awaiting it cannot report PASS for an unsaved account. If the browser session cannot be read, the credential is still saved with session_unreadable and its cause, without cookies or minting timestamps; the next run must sign in again. Product Hunt refuses an existing row with a different source identity or credential, stamps the observed X linkage and binds the saved account to an eligible character. A missing saved account or failed linkage/binding is a failed run; no eligible character is an explicit no_unbound_character outcome, not a fabricated binding. A success line by itself is not proof of the provider account: inspect the retained run and Skarbiec record.
Weles Desktop Runs → Start run uses the managed worker admission for reviewed register actions, then shows the terminal run output and diagnostic artifact. It never reads the operator workstation's Skarbiec or launches a local browser. A worker refusal, source-identity refusal, session capture failure and persistence failure have different meanings; the first two do not establish an account, and session_unreadable retains credentials but not a reusable login.
node tests/registration/account-persistence.mjs runs on the dedicated host with WELES_TEST_DEDICATED_HOST, PROXY_URL, WELES_TEST_REFUSAL_VAULT, WELES_TEST_SUCCESS_VAULT and WELES_TEST_PRODUCTHUNT_HANDLE. Both vault paths must be isolated fixture vaults under build/real-tests/, not operator data. The refusal vault holds one incomplete real X and Instagram source account each; the success vault holds disposable authenticated source accounts and no destination rows. The journey checks refused writes, creates real Product Hunt and Threads accounts, then reads Skarbiec to compare destination handle, email, password and linkage against the source. It retains exact revision, commands, exits, dependency digest, outputs and final item ids under build/real-tests/. The created provider accounts remain in the disposable fixtures; a missing fixture or provider is not a pass.
Run results#
GET /diagnostics/<run_id>/file?path=run-result.json&wait=terminal holds one authenticated read until a detached run has a persisted finished or failed outcome. Weles watches changes to the result directory, including atomic replacement of the result file, instead of reading it on a timer. Without wait, the route still returns the current document immediately.
Only wait=terminal with path=run-result.json is accepted; another value or artifact is refused with HTTP 400. An unknown run remains HTTP 404. A missing, malformed or unreadable result after the read started, or a failed filesystem watch, returns the actual read or watch error. Closing the client connection releases the watch; it does not cancel the detached browser run.
A finished trajectory run's run-result.json holds ok, exitCode, result, outputs, stdout_tail, stderr_tail and cancelled. stdout_tail and stderr_tail hold the run's whole standard output and standard error, not a cut end of them; GET /runs reports the same two fields for a running run with everything written so far. A run ends when the trajectory exits or its caller cancels it; Weles sets no limit on how long it may take. result is the last line the trajectory printed that parses as JSON, or a generic browser task's generic_task_result.json when it printed none.
outputs holds the named documents the run left under recordings/<run_id>/: pangram (pangram_result.json), generic_browser_task (generic_task_result.json), ban_signal (ban_signal.json), pending_review (pending_review.json), service_action (service_action_result.json), capture (capture_result.json), accessibility_audit (accessibility_audit_result.json), captcha (captcha_events.json), overleaf_version_history_summary and yahoo_register. A key is absent when the run wrote no such file. A file that exists but cannot be read or parsed is named in output_errors with its reason, so unreadable is distinguishable from absent; every file is read whole, whatever its size. Symbolic links in the recording tree are not followed.
pangram (the pangram_analyze_text action) holds Pangram's answer for the whole text — verdict (ai_generated, ai_assisted or human: the class with the largest share), ai_percent, human_percent, ai_assisted_percent, avg_ai_likelihood_percent, prediction, prediction_short, headline, source, final_url — and, from the same answer, text (the text Pangram analysed, which it may have normalised from the submitted one) and windows: one entry per segment Pangram labelled, with start_index and end_index (end-exclusive offsets into that text), label, confidence and ai_likelihood (the likelihood under whichever of its two names the API used). A caller that colours words reads windows; an answer in which Pangram labelled no segment has an empty windows array, and an answer with no windows array has windows null. A segment whose offsets are not integers enclosing some text is not placed: it is listed in unreadable_windows with its index and the offsets it carried. Real test: tests/trajectories/pangram-windows.mjs runs the action through stado workload run and checks every window lies inside text and none is unreadable.
ban_signal holds {healthy, signal, details} for the run's account on Instagram, X, TikTok, LinkedIn, Discord, GitHub, Pangram, Product Hunt and Reddit. A Chrome error page is proxy_auth_failed, ip_blocked or proxy_failed. Otherwise the route the site sent the session to (details.matched_url) and the named fields of the platform's API answers the session observed are read from src/platforms/_shared/signals.json, an HTTP Too Many Requests answer is rate_limited, and the page itself is judged by the vision model through Brama against the account states healthy, suspended, rate_limited, captcha_challenge, checkpoint, insufficient_credits and ip_blocked, in any language or wording; details.judged_state is the model's answer. A page whose text cannot be read, a question the model could not answer, or an answer that names no state is unknown_error with details.body_text_error or details.judgement_error, never healthy. A platform signals.json does not declare is refused with “no account signals are declared for <platform> in src/platforms/_shared/signals.json”.
A platform health probe (<platform>_health, written to recordings/<run>/<platform>_health/) reads suspension from the platform API's own fields where it has them (LinkedIn's restricted state) and otherwise from the ban_signal its detector judged on the signed-in page; no health probe matches page wording. Whether the public profile is visible signed out is read from structure only: the HTTP status the profile answers with, the profile metadata the platform declares (Instagram's Open Graph og:type profile, TikTok's rehydration state naming the user, Reddit's about answer naming the account), or nothing: an answer that cannot tell (LinkedIn's refusal statuses for non-members, a TikTok page without its state) leaves visibility unread instead of reading it as hidden, so it never makes an account shadowbanned. A TikTok rehydration state that is not JSON fails the probe as HEALTH_VISIBILITY_UNREADABLE with the parse error.
Gmail search requires the caller's query#
The gmail_login_search action requires params.query or params.q to be a nonempty Gmail search string. There is no account-specific default. The worker maps that input to GM_QUERY and refuses absent, non-string or whitespace-only input with GMAIL_QUERY_REQUIRED before spawning a trajectory. The trajectory checks GM_QUERY again before acquiring credentials or opening a browser.
Use POST /run with action gmail_login_search, params containing query and open: false, and detached: true. Read the terminal run result through the diagnostics route above. A failed input check is a failed run, not a successful search; its error retains GMAIL_QUERY_REQUIRED. Command-line and graphical callers use that same worker contract.
On the Stado-selected worker, run node src/trajectories/gmail/gmail_login_search.mjs with an explicit GM_QUERY for a fresh login, or node src/trajectories/gmail/gmail_search.mjs with an explicit GM_QUERY for a previously established persistent profile. Both refuse a missing or whitespace-only query with GMAIL_QUERY_REQUIRED before opening a browser; neither searches the whole mailbox by default. GM_OPEN=0 lists rows without opening messages. Do not launch these browser entry points on the caller's machine.
node tests/gmail/query-required.mjs exercises local CLI refusals with an isolated home and no credential identity; it never launches a browser. node tests/gmail/search.mjs --query <fixture-query> --expected-subject <fixture-subject> --expected-snippet <fixture-snippet> uses the configured managed executor and its dedicated Gmail fixture. It requires the exact source revision on the worker, checks an input refusal and reads the independently known fixture message without opening it. Reports and supported recordings remain under build/real-tests/. An absent fixture or unavailable executor is blocked, and passing preflight tests does not qualify Gmail authentication or message retrieval.
Meta Marketing API actions require an object and a verb#
Use POST /run with action meta_ads_api_campaign, meta_ads_api_catalog, meta_ads_api_audience, meta_ads_api_creative or meta_ads_api_lead_form. Supply params.action (create, update, read, list or delete where that object supports it) and params.resource for campaign, catalog and creative wrappers. An absent or unsupported choice fails before a Graph request; no object is created by default. Command-line entry points read ACTION_KIND or META_ACTION and RESOURCE from the environment. In Weles Desktop, open Runs → Start run, enter the same worker action and a JSON object of parameters; the form shows the admitted run identifier or the exact HTTP/input refusal, and Runs shows its terminal result and diagnostics.
For a read, submit {"action":"meta_ads_api_campaign","params":{"resource":"campaign","action":"read","campaign_id":"<campaign-id>","fields":"id,name","live_read":true},"detached":true} to POST /run and inspect run-result.json. The integration service must be configured; its HTTP refusal and Graph error are reported by the run. Read fields are only the caller's FIELDS, or Meta's own default when none is stated.
Writes require params.submit: true. Campaign, ad set and ad creates require the advertiser's initial status rather than assuming one; a campaign also requires its name, objective and special-ad-category declaration. Audience create requires audience_subtype and a customer-file audience additionally requires customer_file_source. Updates send only fields stated for the selected resource, or the explicitly provided UPDATE_JSON body; an ad set name cannot rename a campaign. A missing input is refused by name before a write.
A lead form update sends locale only if params.locale was stated. To rename an existing form without changing its language, submit action meta_ads_api_lead_form with params {"action":"update","form_id":"<form-id>","lead_form_name":"<name>","submit":true}; inspect a subsequent read of fields id,name,locale. A missing action is refused before create, and unknown actions name the supported verbs.
node tests/meta/marketing-api.mjs --ad-account <dedicated-account> --business-id <dedicated-business> --lead-form-id <dedicated-form> --lead-form-name <form-name> --lead-form-locale <non-US-locale> --objective <objective> --special-ad-categories <json-array> --name-prefix <test-prefix> --status PAUSED lists the real audience, creative and catalog objects, updates a dedicated non-US lead form without changing its locale, then creates, updates, reads and deletes an isolated campaign through the integration, plus missing-action refusals. It requires integration credentials and dedicated test objects, records the exact source revision and response state under build/real-tests/, and never treats an absent dependency as success.
Google Play release requires a track and explicit rollout#
Use POST /run with action google_play_submit, params containing bundle_path (a bundle accessible on the managed worker), package_name or play_release_url, and track (internal, closed, open or production). Weles refuses a missing or unsupported track before opening Play Console, including inherited object-property names. It never chooses a track. State proxy_url_override for the connection if the worker has no configured proxy.
A run without params.submit: true uploads and advances the release but does not press Start rollout or Send for review. To send an isolated release explicitly, include submit: true and inspect the terminal run-result.json and Play Console release status. A staged message describes the controls reached, not independent proof that Play persisted a draft. The terminal rollout result requires a visible provider status; an absent status is PLAY_RELEASE_OUTCOME_UNOBSERVED, not success. Missing name, notes, track navigation and advance controls name the refused operation.
In Weles Desktop, Runs → Start run accepts google_play_submit with the same JSON parameters and shows the admitted run identifier or input refusal; Runs displays the final status and recordings. On the Stado-selected worker, the direct entry point is node src/trajectories/google/play/play_submit.mjs with BUNDLE_PATH, PACKAGE_NAME or PLAY_RELEASE_URL, TRACK and an explicitly selected proxy; only explicit SUBMIT enables sending. Do not launch its browser on the caller's machine.
node tests/release/play-console.mjs --isolated-package-name <dedicated-test-app> --stage-bundle <fresh-worker-bundle> --rollout-bundle <different-fresh-worker-bundle> --expected-rollout-status <observed-provider-status> --proxy <chosen-connection> exercises missing and inherited track refusals, staging without submit, then an explicit rollout to the isolated internal track. It requires two distinct publishable version codes on the managed worker, retains source revision, responses and supported recordings under build/real-tests/, and cannot qualify a release without real Play Console access.
Google Ads campaigns use advertiser choices#
On the Stado-selected dedicated Weles host, the direct entry point is node src/trajectories/google/ads/ads_campaign.mjs with GOOGLE_ADS_CUSTOMER_ID or ADS_URL, CAMPAIGN_NAME, CAMPAIGN_OBJECTIVE and CAMPAIGN_TYPE stated by the advertiser. An App type also requires APP_CAMPAIGN_SUBTYPE; APP_PLATFORM, APP_ID, APP_NAME and PACKAGE_NAME are used only when explicitly stated. Missing choices are named and refused before browser startup. A provider platform named google is not an app platform and never selects Android or iOS.
POST /run accepts {"action":"google_ads_campaign","account_id":"<dedicated-Google-account-id>","params":{"customer_id":"<dedicated-Ads-customer-id>","campaign_name":"<chosen-name>","campaign_objective":"<chosen-objective>","campaign_type":"<chosen-type>","proxy_url_override":"<chosen-connection>"},"detached":true}. Supply app_campaign_subtype for App campaigns, and app_platform only when the advertiser chose it. Weles Desktop Runs → Start run accepts the same action, account ID and JSON parameters. Its inspector shows the terminal worker refusal or output; the launch sheet shows HTTP and input admission refusals.
Without submit:true, the browser enters the stated choices and verifies the name in the provider's campaign setup page but does not publish. This is not proof that Google saved a draft. An explicitly submitted campaign requires a visible published, created, eligible or review status; a click without that status fails as GOOGLE_ADS_CAMPAIGN_OUTCOME_UNOBSERVED. Read the terminal run-result.json and supported recordings before treating a run as evidence of publication.
node tests/google_ads/campaign.mjs --account-id <isolated-Google-account-id> --customer-id <isolated-Ads-customer-id> --name-prefix <test-name> --objective <provider-objective-label> --type <provider-type-label> --proxy <chosen-connection> runs on the dedicated host with WELES_TEST_DEDICATED_HOST. For an App type also supply --app-subtype <provider-subtype-label>. The isolated Skarbiec account must carry weles_real_test_fixture and authenticated Google Ads cookies. The journey checks missing name, objective, type and App subtype refusals, then reaches the real provider setup form with all choices explicit and observes the requested name without publishing. It retains the exact source revision, terminal results and supported recordings under build/real-tests/; an unavailable account or unexecuted journey is not a pass.
GitHub publishing keeps caller-supplied content#
Use POST /run with action github_create_repo and params repo_name and repo_desc. Both are required before credentials or a browser are acquired. Invalid repository-name characters are refused rather than silently removed; the form must hold exactly the requested name and description before Create repository is pressed. A resulting repository URL is checked against the account and requested name.
Use action github_commit with params repo_url (GitHub URL or owner/name), file_path, commit_message and file_append. Every value is required before browser startup. A URL outside github.com or with an extra path is refused without browsing. The appended text is typed as given at the end of the file, so include any separator or newline in file_append. An absent commit-message field or a field whose readback differs from commit_message is refused before confirmation; GitHub's default message is never accepted as the caller's. A provider refusal or a still-open edit page is a failed run, not a commit.
In Weles Desktop, Runs → Start run accepts either action with the same JSON parameters. The admitted identifier, input refusal, terminal result and recordings use the shared Runs screen. On the Stado-selected worker, the corresponding direct entry points are node src/trajectories/github/content/create_repo.mjs with REPO_NAME and REPO_DESC, and node src/trajectories/github/content/commit.mjs with REPO_URL, FILE_PATH, COMMIT_MESSAGE and FILE_APPEND. Do not launch their browsers on the caller's machine.
node tests/github/publish.mjs --owner <dedicated-public-account> --repo-prefix <isolated-prefix> --repo-description <chosen-description> --existing-repo <dedicated-public-repo> --file-path <existing-file> --append-prefix <chosen-text> --commit-message <chosen-message> checks missing-input refusals and a real repository creation and commit. It reads the new repository, changed file and latest commit from GitHub after the worker run, retaining exact source revision, commands and recordings under build/real-tests/. Use a disposable account and repository: the newly created repository and test commit remain in that fixture's history.
Overleaf GitHub sync records the caller's message#
On the Stado-selected dedicated Weles host, run OVERLEAF_COMMIT_MESSAGE=<chosen-message> node src/trajectories/overleaf/push_github.mjs <project-id> <owner/repository>. The project and linked repository disambiguate the target. An absent or empty OVERLEAF_COMMIT_MESSAGE refuses before credentials or browser startup; the commit dialog must hold the exact requested text before Sync is pressed. Overleaf's settled interface is not independent proof of a GitHub commit: inspect the linked repository's history.
POST /run accepts {"action":"overleaf_push_github","params":{"project":"<project-id>","repo_slug":"<owner/repository>","commit_message":"<chosen-message>"},"detached":true}. Weles Desktop Runs → Start run sends the same action and JSON parameters; the inspector shows terminal refusals and worker output. A missing message names OVERLEAF_COMMIT_MESSAGE, and a mismatched field names the value it held. A linked-repository mismatch or provider conflict fails with the observed cause.
node tests/overleaf/push_github.mjs --project-id <isolated-linked-project> --repo-slug <dedicated-public-repository> --commit-message <unique-message> runs on the dedicated host with WELES_TEST_DEDICATED_HOST. Prepare a pending edit in the isolated Overleaf project linked to that disposable GitHub repository. The journey checks the missing-message refusal, runs the real sync, then reads GitHub's default-branch commit independently and requires a new SHA and the exact message. It retains source revision, worker terminal statuses and supported recordings under build/real-tests/. Do not use an operator repository: the test commit remains in the fixture's history; an unavailable provider or unexecuted journey is not a pass.
Profile edits keep unstated fields and require a confirmed write#
On the dedicated Weles host, run ACCOUNT_ID=<exact-Skarbiec-account-id> node src/trajectories/linkedin/actions/edit_profile.mjs to apply the linked character's stated full name, headline, About and avatar. The corresponding X, Instagram and TikTok entry points are src/trajectories/<platform>/actions/edit_profile.mjs; Reddit is src/trajectories/reddit/content/edit_profile.mjs. ACCOUNT_ID selects the exact account rather than the first fresh account. A conflicting WELES_LOGIN_ITEM or ACCOUNT_ITEM refuses the run by naming both bindings. An unstated name is not written to the provider and does not clear the stored display_name.
The worker accepts POST /run with {"action":"linkedin_edit_profile","account_id":"<exact-Skarbiec-account-id>","params":{},"detached":true}; substitute twitter, instagram, reddit or tiktok for the platform. Weles Desktop Runs → Start run has an account-ID field beside action and parameters and sends the same account_id. A missing stated field, an input that does not hold the exact requested value after clear and fill, a disabled Save control, a provider refusal or an absent avatar control fails with its observed cause. LinkedIn's About and intro are reopened after Save for persisted readback. A successful click or upload input alone is not an independently verified provider profile; inspect the terminal run result and recording.
node tests/profiles/character.mjs --account-id <disposable-LinkedIn-account-id> runs on the Stado-selected dedicated Weles host with WELES_TEST_DEDICATED_HOST matching that host. The Skarbiec account must be marked weles_real_test_fixture and have authenticated cookies, a full-name character, About and a requested avatar distinct from its current provider photo. The journey checks an absent-account refusal, a real profile write and independent provider form/photo readback, then a missing-name preservation run and a single-name refusal. It restores the fixture character and retains source revision, worker commands and statuses, provider observations and recordings under build/real-tests/. An unavailable dependency or unexecuted journey is not a pass.
Apple Ads report harvest preserves unknown figures#
On the dedicated Weles host, run ACCOUNT_ID=<isolated-Apple-account-id> APPLE_ADS_APP_ID=<app-id> APPLE_ADS_DATE_PRESETS=<comma-separated-provider-labels> APPLE_ADS_EXACT_RANGES=<label=YYYY-MM-DD..YYYY-MM-DD,...> node src/trajectories/apple/ads/report_harvest.mjs. The app, date presets and exact ranges are caller choices. An explicitly authorized Apple sign-in must already have established the selected account session; this read does not sign in or change campaigns.
POST /run accepts {"action":"apple_ads_report_harvest","account_id":"<isolated-Apple-account-id>","params":{"apple_ads_app_id":"<app-id>","apple_ads_date_presets":"<provider-labels>","apple_ads_exact_ranges":"<label=YYYY-MM-DD..YYYY-MM-DD>"},"detached":true}. Weles Desktop Runs → Start run sends the same account ID and JSON parameters to the managed worker. Its terminal result exposes outputs.apple_ads_report_harvest, containing Apple's raw exact-range answers and their summary. The worker closes its browser when the report ends; a direct CLI run keeps it open for review unless APPLE_ADS_CLOSE_AFTER_HARVEST is enabled.
Each requested range must have a captured request template, an observable successful HTTP response, parseable JSON, no GraphQL errors and campaign rows. A fetch or interpretation failure reports the range, HTTP status or missing response, GraphQL errors and the observed read/parse cause; the run fails and report_harvest.json records ok:false rather than a previous successful result. Input refusals before startup appear in the terminal run outcome. A figure Apple omitted is null, not zero; spend currency is Apple's stated currency or null, never an assumed USD. Compare summary.exactReports with exactReports before using a report for spending decisions.
node tests/apple_ads/report_harvest.mjs --account-id <isolated-Apple-account-id> --app-id <read-only-app-id> --date-presets <provider-labels> --exact-ranges <label=YYYY-MM-DD..YYYY-MM-DD> --rejected-range <provider-rejected-label=YYYY-MM-DD..YYYY-MM-DD> runs on the Stado-selected dedicated host with WELES_TEST_DEDICATED_HOST. The Skarbiec account must carry weles_real_test_fixture and an authenticated Apple Ads session; choose the rejected range from a real provider refusal, not a simulated answer. The journey checks missing and malformed range refusals, a real rejected GraphQL range with its HTTP/cause and failed artifact, then a real provider report with raw figures against the persisted summary. Source revision, terminal statuses and supported recordings are retained under build/real-tests/. An unavailable account or unexecuted journey is not a pass.