Artwork Lady — Embroidery Digitizing intake
Art work Lady digitizes embroidery artwork, and the only way in is a web
intake form on https://www.artworklady.com/placeorder — there is no API.
This skill drives that form with Playwright. It is the portal adapter half of
the digitizing loop; the ERP half (poll digitize.demand, download the files,
update the record) lives in the routine that calls it.
The single helper is scripts/artworklady_digitizing.py:
# Read-only diagnostic: log in, open the Embroidery tab, dump the live field
# map + a screenshot. Nothing is submitted. Run this first if the form changed.
python3 scripts/artworklady_digitizing.py capture '{}'
# Fill the form — DRY RUN by default (fills + validates, does NOT click Send).
python3 scripts/artworklady_digitizing.py submit '{
"job_reference": "12345",
"turnaround": "sameday",
"file_types": ["dst"],
"color_scheme": "CMYK",
"files": ["/path/artwork.png", "/path/spec.pdf"],
"message": "Left chest, 3.5in wide. Threads: white + navy. Digitize to DST."
}'
# Actually post the job — clicks Send and confirms via the submit response.
python3 scripts/artworklady_digitizing.py submit '{ … , "confirm": true }'
# Fetch the FINISHED files for a completed job (the return leg). Logs in,
# opens /orders, finds our row by job reference, and downloads that row's own
# download links — so the file is whatever the vendor named it, never a guess.
python3 scripts/artworklady_digitizing.py fetch '{"job_reference": "8524", "out_dir": "/tmp/dl"}'
Each command prints one JSON object, or {"error": {...}} with a non-zero
exit. Credentials come from ARTWORKLADY_USERNAME / ARTWORKLADY_PASSWORD
(or inline username/password); if missing it exits config_error — stop
and tell the user to configure them, never ask for them in chat.
submit input
| Key | Required | Default | Notes |
|---|---|---|---|
job_reference |
✅ | — | The intake "Job reference". Set it to the digitize.demand / decoration id. |
files |
✅ | — | 1–3 local paths. The first fills the mandatory "Upload File Here"; the rest are optional. |
turnaround |
✅ | — | sameday | nextday | rushorder. Required — the caller supplies it per demand (no baked-in default). |
file_types |
["dst"] |
Any of dst | emb | exp | other (≥1). For other, also pass other_text. |
|
color_scheme |
CMYK |
CMYK | RGB. (Present on the form though embroidery is thread-based.) |
|
message |
"" |
Free text — the digitizing brief (dimensions, placement, thread colors, garment…). | |
proof_template |
false |
Ticks "Required proof template". | |
other_text |
— | Required when file_types includes other. |
|
admin_note |
— | Optional hidden note posted with the job. | |
confirm |
false |
false = dry run (fill + validate, no Send). true = post the job. |
|
login_type |
env/employee |
employee | company. |
|
headless, base_url, state_path, timeout_ms, artifact_dir |
env | Overrides for the env vars above. |
The value mapping (which turnaround, which file type, what the message says, which two Odoo attachments to send) is the routine's decision — this skill just takes the values. The defaults above are placeholders to confirm.
fetch input / output — the return leg
fetch downloads a completed order's files from /orders by job reference,
using that order row's own download links (never a guessed URL).
| Key | Required | Default | Notes |
|---|---|---|---|
job_reference |
✅ | — | The job reference to find (the decoration id). |
order_id |
— | Disambiguates when a job reference repeats across your orders (the Artwork Lady Order Id). | |
out_dir |
cwd / artifact_dir | Where the files are written. | |
exts |
all | Restrict to these extensions, e.g. ["dst","pdf"]. |
|
require_completed |
true |
Only pull a row marked Completed. |
{ "status": "fetched", "job_reference": "8524", "order_id": "68217",
"files": [ {"name":"8524.DST","path":"…","size":16878,"ext":"dst","url":"…"}, … ],
"dst": "/abs/path/8524.DST" }
{ "status": "pending", "job_reference": "8524", "message": "…not ready yet" }
{ "status": "not_found", "job_reference": "8524" }
Take the finished stitch file from dst. Why scrape rather than guess the
URL: the vendor's file store is shared across customers, so a job-number
collision can make it rename the file (e.g. 8524_2.DST). The /orders row
always links to the right file for your order, whatever it is named.
What "success" means — read before wiring the status update
The Send button (input[name=submit], onclick="uploadfile()") is not a
native form submit. uploadfile() validates, then POSTs a multipart/form-data
XHR to https://www.artworklady.com/placeorder/order_form_submit, and on the
XHR's load event it writes the response into #status and redirects to
/placeorder/thanks — without checking the HTTP status. So the redirect
alone is not proof of success.
This skill therefore judges success on the actual order_form_submit
response: it waits for that POST, and treats the job as posted only on
HTTP 200, returning status: "submitted" with the response body and the
/thanks URL. A non-200 raises portal_error. The routine must update
digitize.demand only on status: "submitted" — never on a dry_run, an
error, or the mere sight of /thanks.
A live run confirmed the endpoint returns HTTP 200 with an empty body — no
confirmation/ticket id comes back (and /thanks is static), so there is nothing
to correlate a submission to. De-duplication is the caller's job: stamp the
demand with its job_reference + a submitted-at time on submitted, and skip
demands that already carry one, so a routine retry after a crash can't double-post.
Result shapes:
{ "status": "dry_run", "filled": { … }, "screenshot": "…", "trace": [ … ] }
{ "status": "submitted", "filled": { … }, "response": {"status":200,"body":"…"}, "thanks_url": "…/placeorder/thanks" }
{ "error": { "type": "config_error|portal_error|unexpected", "message": "…" } }
Where this fits
routine: poll Odoo digitize.demand → download attachments to disk
→ THIS SKILL: submit {job_reference, values, files, confirm:true}
→ on status=="submitted": update the digitize.demand record in Odoo
Keep the exactly-once discipline the same way the payables skills do: submit
first, update Odoo after a submitted result. If the routine crashes
between the two, the demand is still "to digitize" and will be retried — so
decide up front how to avoid a double-post (e.g. store the returned
confirmation / a submitted-at stamp on the demand, and skip demands that
already carry one). The portal itself does not dedupe.
Notes / gotchas (see references for the full map)
- Employee vs Company login. The popup opens on Register; the driver clicks
"Sign in" (
switchForm('login')) and logs in asemployee(the confirmed default).companyis the other toggle. - One shared form, three tabs. All three tabs are the same
#contactform;change_form('embroidery')just unhides the digitizing section and sets the hidden#form_type=1. The driver asserts that before filling. - Color scheme id is swapped between tabs.
change_formmoves the idcolorSchemebetween the CMYK/RGB select and the printing-method select, so the driver targets the CMYK/RGB one by its stable classselect.color_scheme, never the id. fileTypevalues repeat across the hidden tabs, butdst/emb/exp/otherare unique to the embroidery section and the only visible ones after the tab switch.file1is mandatory; a turnaround radio must be checked (the page throws otherwise). The driver mirrors these preconditions invalidate_ready()and refuses to Send until they hold.
Build status
Confirmed live end to end, including a real Send: capture, submit (dry
run), and one confirm: true submission have all run against the live portal
from this environment — login (employee) → /placeorder → Embroidery tab →
every field filled and validated → Send → order_form_submit HTTP 200 →
/placeorder/thanks, in ~11s headless. The one real submission used a throwaway
job_reference ("AUTOMATION-TEST-PLEASE-IGNORE") and a "please ignore" message,
and should be deleted from the Artwork Lady account. Untested: login from a
long-lived datacenter IP, and whether the vendor ever adds a bot/MFA check.
References
references/placeorder_flow_notes.md— the reverse-engineered flow: every selector, theuploadfile()submit mechanism, the success signal, and the gotchas. Start here if the form changes.
