Snowflake Marketplace Listing Playbook¶
Operational SOP for shipping new Marketplace listings or resubmitting after rejection. Distilled from 14+ manifest rejection cycles during the 2026-04-20→23 sprint, plus the 2026-04-28 real-data-leak incident and Paid bundle 510404 gate discovery.
This document is SSOT for execution. The Claude skill at .claude/skills/snowflake-listing-submit/ is a thin pointer to this file.
Prerequisites¶
- Roles: account has split —
ACCOUNTADMINforCREATE SHAREand grants on share contents;ORGADMINforCREATE EXTERNAL LISTING,ALTER LISTING,ALTER PROFILE. Multi-phase scripts mustUSE ROLEbetween phases or fail with "Insufficient privileges to operate on account". - Provider profile: must be APPROVED (or at least PENDING, not REJECTED) for any listing to publish. Profile lives in
SNOWFLAKE_DATA_MARKETPLACEdata exchange. Check viaSHOW PROFILES IN DATA EXCHANGE SNOWFLAKE_DATA_MARKETPLACE. - Share: target share must exist with
secure_objects_only = TRUE. Example:HUSKY_MOBILE_SAMPLEwraps views inHUSKY_DATA.SHARE. - Snowflake CLI (
snow) configured against~/.snowflake/connections.tomlprofileHusky(org=ciqcvik, account=ujb07324, region AWS_US_WEST_2). Snowsight Worksheets UI is brittle for paste-large-SQL — always prefer CLI for multi-statement scripts. - Account billing/service address = SG (151 Chin Swee Road, #07-12 Manhattan House, Singapore 169876) for any Paid listing. HK address triggers 510404 — see Failure Mode #2.
Inputs (gather before opening any SQL)¶
- Listing identity:
<NAME>,<TITLE>,<SUBTITLE ≤110 chars> - Access type:
FREE|LIMITED_TRIAL|PAID(decide per § Access Type Decision Tree) - Share name: existing or new (if new,
CREATE SHAREfirst under ACCOUNTADMIN) - Share contents: enumerate views/tables. Verify row counts vs access tier (see Failure Mode #1)
- Trial duration (if
LIMITED_TRIAL): one of1 / 7 / 30 / 60 / 90days - Pricing plan YAML (if
PAID): pre-staged in@HUSKY_DATA.PUBLIC.LISTING_MANIFESTS/<folder>/ - Geo coverage: countries list using Snowflake-canonical names (uppercase, human-readable; see § YAML schema)
Workflow¶
Phase 0 — Pre-flight (mandatory, no exceptions)¶
# 1. Confirm CLI connection works
snow sql --connection Husky -q "SELECT CURRENT_ACCOUNT(), CURRENT_REGION(), CURRENT_VERSION()"
# 2. Snapshot current listings state
snow sql --connection Husky --format json -q "USE ROLE ORGADMIN; SHOW LISTINGS;" \
| python3 -c "
import json, sys
data = json.load(sys.stdin)
listings = data[1] if len(data) > 1 else data
for L in sorted(listings, key=lambda x: x['name']):
print(f\"{L['name']:55} state={L.get('state','-'):10} review={L.get('review_state') or '-'}\")
"
# 3. For each share referenced, verify row counts match access tier
snow sql --connection Husky -q "
USE ROLE ACCOUNTADMIN;
USE DATABASE HUSKY_DATA; USE SCHEMA SHARE;
SELECT 'V_HEM_US_SAMPLE' v, COUNT(*) c FROM V_HEM_US_SAMPLE
UNION ALL SELECT 'V_MAID_APAC_PLUS_US_SAMPLE', COUNT(*) FROM V_MAID_APAC_PLUS_US_SAMPLE
UNION ALL SELECT 'SEGMENT_CATALOG', COUNT(*) FROM SEGMENT_CATALOG
;"
🛑 Hard stop: if any view bound to a Free/Trial share has row_count ≥ 10M, abort. See Failure Mode #1.
Phase 1 — Reverse-engineer schema before writing¶
Snowflake's manifest YAML has undocumented enum traps. Don't blind-guess:
Pulls the canonical YAML Snowflake stores. When making a change:
- Open Snowsight (as ORGADMIN), make the change in UI
- Re-run dump →
/tmp/after.yaml diff /tmp/baseline.yaml /tmp/after.yaml→ exact field name and shape
This pattern was the breakthrough for most of the sprint's schema traps. Also try WebFetch on docs.snowflake.com/en/progaccess/listing-manifest-reference — verified accessible 2026-04-25 (earlier "blocked" claim was wrong; always retry before guessing).
Phase 2 — Author the manifest¶
Write manifest to /tmp/lst<NN>-<purpose>.yaml. Do NOT inline manifest in CREATE statement until validated.
Minimal manifest skeleton (Free / Limited Trial; verified shape 2026-04-25):
title: "..."
subtitle: "..." # ≤110 chars
description: |
Multi-line markdown OK. NO TABLES (UI renders raw pipes).
Use **bold** + bullets only.
profile: "<profile_name_exact_including_uuid>"
listing_terms:
type: "STANDARD" # STANDARD | CUSTOM | OFFLINE
categories:
- MARKETING # MARKETING | IDENTITY | ANALYTICS TOOLS | CORTEX AI READY
business_needs:
- name: "Audience Activation" # only confirmed-working enum value
description: "..."
data_attributes:
refresh_rate:
update_frequency: MONTHLY # HOURLY | DAILY | WEEKLY | MONTHLY | YEARLY | EVENT_BASED
geography:
geo_option: COUNTRIES # NOT_APPLICABLE | GLOBAL | COUNTRIES
granularity: [COUNTRY] # COUNTRY | LATITUDE_LONGITUDE | ADDRESS | POSTAL_CODE | CITY | COUNTY | STATE | REGION_CONTINENT
coverage:
states: [] # required field even when empty
continents:
ASIA: [HONG KONG, INDIA, JAPAN, SOUTH KOREA, SINGAPORE, MALAYSIA, INDONESIA, PHILIPPINES, THAILAND, VIETNAM]
NORTH AMERICA: [UNITED STATES]
OCEANIA: [AUSTRALIA, NEW ZEALAND]
usage_examples:
- title: "..."
description: "..."
query: "SELECT * FROM SHARE.V_HEM_US_SAMPLE LIMIT 10;" # 2-part name only
resources:
documentation: "https://huskydata.io/products/<sku>" # product-specific URL, not /segments
# For Limited Trial only:
limited_trial_plan:
trial_time_limit: 30
For Paid, see § Paid Listing Path — different structure (manifest + pricing_plan files staged).
Phase 3 — Create or alter listing¶
New listing (under ORGADMIN):
USE ROLE ORGADMIN;
CREATE EXTERNAL LISTING <NAME>
SHARE <SHARE_NAME>
AS $$
-- paste manifest YAML here, using $$ dollar-quotes (avoids apostrophe escaping)
$$
PUBLISH=FALSE REVIEW=FALSE; -- DRAFT state, not yet submitted
Resubmit existing listing with revised manifest:
USE ROLE ORGADMIN;
ALTER LISTING <NAME>
AS $$
...
$$
PUBLISH=TRUE REVIEW=TRUE; -- submit for review + auto-publish on approval
🛑 The combo PUBLISH=TRUE REVIEW=FALSE always errors: "publish needs to be FALSE when requesting a listing to not be reviewed". Linked-boolean rule:
(PUBLISH, REVIEW) |
Effect |
|---|---|
(FALSE, FALSE) |
DRAFT / unsubmitted |
(FALSE, TRUE) |
Submit for review only — manual publish after approval |
(TRUE, TRUE) |
Submit for review + auto-publish on approval |
(TRUE, FALSE) |
❌ Error |
Phase 4 — Submit for review (if not done in Phase 3)¶
Use this when Phase 3 was PUBLISH=FALSE REVIEW=FALSE and you now want to submit. UI "Re-submit" button has extra preflight gates (Region availability, Access type, Publishing profile) that can block; SQL ALTER LISTING REVIEW bypasses UI preflight and submits directly. Review still happens.
Phase 5 — Verify state¶
Confirm: state, review_state, is_limited_trial (if Trial), manifest_yaml matches submission.
Phase 6 — Update wiki state¶
Update the listing-state table in docs/strategy/2026-04-23-publishing-strategy.md (and any project pages that reference current marketplace state). Do not skip — wiki SSOT drift is a known operational failure mode.
Standards¶
✅ Must do¶
- Always run Phase 0 row-count check before any submit. The 2026-04-28 incident exists because this was skipped.
- Use
snow sql -c HuskyCLI, not Snowsight Worksheets UI for multi-statement scripts. - Use
$$...$$dollar-quotes for inline manifest YAML, never'...'. - Country names UPPERCASE human-readable:
UNITED STATESnotUSA/US.HONG KONGnotHK. - Continent names UPPERCASE space-separated:
NORTH AMERICAnotNORTH_AMERICA. coverage.states: []must always be present, even when empty. Otherwise: "When Geo Attributues has COUNTRIES, Coverage needs to have US states or Countries information" — note typo "Attributues" in actual error string is authoritative.- For Trial:
limited_trial_plan: trial_time_limit: <int>. NOTtrial_details:(latter doesn't flipis_limited_trialflag — empirically verified 2026-04-28 against #60 + #61). USE ROLEbetween phases when script crosses ACCOUNTADMIN ↔ ORGADMIN boundary.- Stick to
business_needs[].name = "Audience Activation"unless you have rollback time budgeted. - Documentation URL must be product-specific:
huskydata.io/products/<sku>, not generic/segments. Reviewer rejects on this.
❌ Must not¶
- ❌ Put real production views (
V_HEM_US910M,V_MAID_APAC_PLUS_US33.6M) into any Free or Limited Trial share. Hard red line — see Failure Mode #1. - ❌ Use
CREATE LISTING(noEXTERNAL) — syntax error. - ❌ Use
CREATE OR REPLACE LISTING— unsupported. UseDROP LISTING IF EXISTSthen CREATE if you need to replace. - ❌ Add
DISTRIBUTION = EXTERNALclause —EXTERNALkeyword in command name already sets it. - ❌ Try
ALTER LISTING ... SET ACCESS='LIMITED_TRIAL'— onlySET COMMENTis supported. Access type flips via manifestlimited_trial_planblock + ALTER LISTING with new YAML. - ❌ Speculatively add optional YAML sections (
data_dictionary,resources.media,support_contact,approver_contact) to a draft. Each unknown field = at least one rejection cycle. - ❌ Set
targets.regions: ALLat CREATE time — fires090857 Autofulfillment must be specified. Set single region (PUBLIC.AWS_US_WEST_2) at create, expand via UI later. - ❌ Mention any of: AlikeAudience / Experian / Hotmob / Acxiom / Eyeota in manifest text. (Supplier confidentiality black-list.)
- ❌ Use markdown tables in
description— UI renders pipes literally. Use bullets +**bold**headings only. - ❌ Use 3-part names (
HUSKY_DATA.SHARE.V_HEM_US) inusage_examples.query. Consumer chooses DB name at mount time — useSHARE.V_HEM_US(2-part).
Access Type Decision Tree¶
| Data character | Access type |
|---|---|
| Reference (catalog, crosswalk, coverage rollup) | FREE |
| Sample / pre-purchase taste (≤1M rows, deterministic hash) | LIMITED_TRIAL |
| Real production audience data | NOT on Marketplace — sales@huskydata.io direct contract OR is_by_request=true (request-approval flow) |
| Paid pay-per-mount | PAID with Stripe Express + Marketplace Ops onboarding (see Paid Listing Path) |
🛑 Reviewer feedback "submit as Limited Trial" is a trap. Reviewer cares about listing-type ↔ description consistency, NOT whether real data leaks. If reviewer says flip to Trial, the right move is rebuild share with sample views, NOT flip access type while real data still attached.
Sample views — deterministic hash pattern¶
For Trial shares, build sample views with reproducible-but-bounded output:
-- HUSKY_DATA.SHARE.V_HEM_US_SAMPLE — ~991 rows
CREATE OR REPLACE VIEW HUSKY_DATA.SHARE.V_HEM_US_SAMPLE AS
SELECT * FROM V_HEM_US WHERE ABS(HASH(hem)) % 1000000 < 1;
-- HUSKY_DATA.SHARE.V_MAID_APAC_PLUS_US_SAMPLE — ~1051 rows
CREATE OR REPLACE VIEW HUSKY_DATA.SHARE.V_MAID_APAC_PLUS_US_SAMPLE AS
SELECT * FROM V_MAID_APAC_PLUS_US WHERE ABS(HASH(maid)) % 30000 < 1;
Same query → same rows on repeat. Built for reproducible schema/quality/coverage evaluation by buyers, not freshness. Consumer wanting fresh production → upgrade to Paid.
Output¶
- Listing in
state=PUBLISHED, review_state=APPROVED(orPENDINGwaiting on reviewer) - Manifest YAML committed to
docs/internal/snowflake-listings/<listing>-<YYYY-MM-DD>.yamlfor traceability - Listing-state table updated in
docs/strategy/2026-04-23-publishing-strategy.md - This playbook's
last_revieweddate bumped if any new gotcha discovered (and Failure Modes appended)
Review¶
Self-check (must pass before reporting done)¶
- Phase 0 row-count check executed and output captured
-
DESC LISTING <name>confirmsis_limited_trialmatches intent - Manifest YAML committed to repo (not just
/tmp/) - Wiki state table updated
- If new error / quirk encountered: appended to § Failure Modes with date
Human sign-off gate (Benjamin must approve before)¶
- First-time Paid listing submit (510404 path)
- Any access-type flip (Free → Trial, Trial → Paid)
- Any share content change that touches
V_HEM_*orV_MAID_*real-data views - First-time submit of a listing whose share contains a view ≥10M rows
For routine resubmits (manifest copy edits, geo expansion, retry after reviewer feedback), the agent can self-approve and report.
Failure Modes¶
Each entry: pattern → root cause → fix.
#1 (CRITICAL) — Real production view in Free/Trial share¶
Pattern: Reviewer feedback says "flip to Limited Trial", autopilot flips Access Type without rebuilding share content. Real V_HEM_US (910M rows) or V_MAID_APAC_PLUS_US (33.6M) gets auto-mounted by trial consumers within 24hr SLA.
Why it's a red line (per owner): "一見光就人地用左唔會買。佢可以開好多 account 得閒拎我地 data". Marketplace trial = zero-friction self-serve mount = data permanently leaked.
Fix (executed 2026-04-28):
USE ROLE ACCOUNTADMIN;
REVOKE SELECT ON VIEW HUSKY_DATA.SHARE.V_HEM_US FROM SHARE HUSKY_MOBILE_TRIAL_US;
REVOKE SELECT ON VIEW HUSKY_DATA.SHARE.V_MAID_APAC_PLUS_US FROM SHARE HUSKY_MOBILE_TRIAL_CROSSBORDER;
-- ...regrant only sample views
GRANT SELECT ON VIEW HUSKY_DATA.SHARE.V_HEM_US_SAMPLE TO SHARE HUSKY_MOBILE_TRIAL_US;
#2 — 510404 Account not supported for Marketplace Purchases¶
Pattern: CREATE EXTERNAL LISTING ... FROM '@stage/<paid_folder>' (manifest contains pricing_plans) returns:
510404 (22000): Account not supported for Marketplace Purchases.
Please contact support to resolve this issue.
Root cause: account billing/service address is HK (legacy snapshot), Marketplace Purchases eligible-country list excludes HK. Husky entity is SG-incorporated. Same root cause as AWS Marketplace seller appeal — both external marketplaces have HK on file from old account creation.
Fix order:
1. Update Snowsight Admin > Billing > Service+Billing address from HK → SG (151 Chin Swee Road, #07-12 Manhattan House, Singapore 169876)
2. Wait 24-72hr for propagation
3. Re-test via CREATE EXTERNAL LISTING ... FROM '@stage' with pricing_plans block
4. Only if still gated, file Marketplace Ops case explicitly citing the address update
Eligible countries: AU, CA, CO, FI, FR, DE, IE, IL, IT, JP, KSA, MX, NL, NZ, NO, SG, SE, CH, UK, US.
#3 — 090857 Autofulfillment must be specified¶
Pattern: CREATE EXTERNAL LISTING ... AS $$ targets: regions: ALL $$ errors.
Root cause: Multi-region requires CROSS_CLOUD_AUTO_FULFILLMENT_ENABLEMENT privilege not granted by default.
Fix: Set single region at create (targets: regions: ["PUBLIC.AWS_US_WEST_2"]), expand via UI Provider Studio > Region Availability after listing exists.
#4 — Subtitle must be under 110 chars¶
Pattern: Manifest validation rejects subtitle.
Root cause: hard cap 110 chars, Unicode counts, em-dashes count.
Fix: trim. Common pattern: drop redundant qualifier ("APAC" / "Mobile") since it appears in title.
#5 — Schema traps table¶
| Trap | Symptom | Fix |
|---|---|---|
CREATE LISTING (no EXTERNAL) |
syntax error | Use CREATE EXTERNAL LISTING |
CREATE OR REPLACE EXTERNAL LISTING |
unsupported | DROP LISTING IF EXISTS then CREATE |
DISTRIBUTION = EXTERNAL clause |
redundant — EXTERNAL keyword in command sets it |
Remove the clause |
(PUBLISH, REVIEW) = (TRUE, FALSE) |
"publish needs to be FALSE when requesting a listing to not be reviewed" | Use linked-boolean combos: (F,F) / (F,T) / (T,T) only |
ALTER LISTING SET ACCESS='...' |
only SET COMMENT supported | Flip access via ALTER LISTING <n> AS $$<yaml with limited_trial_plan>$$ PUBLISH=TRUE REVIEW=TRUE |
trial_details: block |
doesn't flip is_limited_trial flag |
Use limited_trial_plan: { trial_time_limit: <int> } |
access: / access_type: in manifest |
"Unrecognized field access" | Access not a manifest field at top level — see limited_trial_plan for Trial, pricing_plans for Paid |
regions: in manifest top-level |
"Unrecognized field regions" | Regions set via targets: block or UI Region Availability |
Markdown tables in description |
UI renders \| pipes \| raw \| literally |
Use bullet lists + **bold** headings |
data_dictionary.featured as array |
"Cannot deserialize from Array" | Use single object {database, objects}, not list. Or omit entirely. |
data_dictionary.objects[].type: VIEW |
"Unrecognized field type" | Omit type — Snowflake infers |
resources.media non-YouTube URL |
"Not a valid media link" | Only YouTube. Omit if no video. |
support_contact / approver_contact |
"not allowed in external listings" | Remove — these come from provider profile for public Marketplace |
categories: ["ADVERTISING_AND_MARKETING"] |
"Invalid category" | Enum: MARKETING / IDENTITY / ANALYTICS TOOLS / CORTEX AI READY. Reject message lists full set. |
business_needs[].name = "Audience Sizing" etc |
"not in standard business needs" | Only "Audience Activation" confirmed working |
3-part names in usage_examples.query |
"Database names cannot appear" | SHARE.V_MAID not HUSKY_DATA.SHARE.V_MAID |
data_attributes missing geography AND time |
"Geo or Time attributes required" | Include at least one. geography: { geo_option: NOT_APPLICABLE } is escape hatch |
data_attributes.time.time_range: "MONTHLY" (string) |
internal error (expects object) | Avoid time_range; use geo_option: NOT_APPLICABLE instead |
coverage missing states: [] with geo_option: COUNTRIES |
"When Geo Attributues has COUNTRIES…" (typo authoritative) | Add coverage: { states: [], continents: {...} } |
documentation URL = /segments |
reviewer rejects "not product-specific" | Use huskydata.io/products/<sku> |
#6 — Provider profile literal \n rendering¶
Pattern: Provider profile description shows literal \n strings instead of newlines on Marketplace public page.
Root cause: profile UI doesn't interpret escape sequences in some fields.
Fix: re-submit profile with actual newlines (paste directly, don't escape).
#7 — Profile PROFILE_REQUIREMENTS_LINKS¶
Pattern: profile rejected on privacyUrl.
Root cause: Snowflake requires dedicated privacy page (must render personal-data / GDPR / CCPA / data-subject-rights / retention / opt-out keywords), not a generic compliance landing page.
Fix: point privacyUrl to https://huskydata.io/company/privacy (dedicated page) → re-submit profile via UI Update Profile.
Paid Listing Path¶
Paid listings have an additional gate (Failure Mode #2) and require Stripe Express setup:
- Verify account address = SG (Admin > Billing > Service+Billing). HK address blocks at 510404.
- File Marketplace Operations case via provider-onboarding-case form requesting "Enable Marketplace Purchases on account
ciqcvik-ujb07324". - Stripe Express setup: Snowsight Admin > Billing > Marketplace billing → register with Husky Technology Pte Ltd details + MFA. (Tab is invisible until step 2 completes.)
- Wait Snowflake Partner Manager engagement + commercial review.
- Stripe payout method "Completed & verified" — 3-14 working days typical.
- Pre-stage manifest + pricing plan files:
- CREATE EXTERNAL LISTING via stage path:
Pricing plan YAML (FLAT_FEE):
display_name: "..."
currency: USD
pricing_model: FLAT_FEE
base_fee: 20000.0
billing_duration_months: 1
sales_motion: SELF_SERVE
metadata:
description: "..."
price: "$20,000 / month"
button_text: "Subscribe"
value_propositions: ["...", "..."]
visibility: VISIBLE
state: PUBLISHED
Manifest references plan via:
pricing_plans:
- name: PLAN_NAME_UPPERCASE
type: FILE
path: pricing_plan_monthly.yaml # relative to manifest folder
Pre-staging is safe before enablement: stage upload + share grants can all be ready. Listing creation will 510404 until step 2-5 complete; one CREATE statement away from live afterwards.
Profile lifecycle¶
UNSENT (draft_status)
└─► PENDING (ALTER PROFILE SET METADATA = '...' then submit via UI)
├─► APPROVED (live — listings can publish)
└─► REJECTED — reject reason in `rejected_reason` JSON column
└─► ALTER PROFILE SET METADATA = '...' + submit via UI Update Profile flow → PENDING again
Profile metadata update via SQL:
Extract current metadata first via SHOW PROFILES IN DATA EXCHANGE SNOWFLAKE_DATA_MARKETPLACE, patch the field (e.g. privacyUrl), write back. UI re-submit still required — there is NO SQL ALTER PROFILE SUBMIT equivalent.
Listing lifecycle¶
DRAFT/UNSENT
└─► DRAFT/PENDING (ALTER LISTING ... REVIEW)
├─► APPROVED → PUBLISHED (ALTER LISTING ... PUBLISH)
└─► DRAFT/REJECTED — reason in rejection_reason column
└─► ALTER LISTING ... AS $$ ... $$ REVIEW
Known listing reject reasons:
- LISTING_PRACTICES_LISTING_TYPE: Free listings should expose full production datasets. If your share has samples, flip to Limited Trial WITH limited_trial_plan in manifest AND ensure share contents match (Failure Mode #1).
- Free-with-trial-data: description implies trial-like behavior on a Free listing → either rewrite description OR flip to Limited Trial properly.
Role-switching gotcha¶
The Husky Snowflake account defaults to ACCOUNTADMIN. Every listing UI interaction requires switching to ORGADMIN via Account Menu → Switch Role. If left on ACCOUNTADMIN, "Edit" / "Submit" buttons are disabled with misleading "no permission" messages, masking real state.
CLI sessions: USE ROLE ORGADMIN at top of each script (or before each phase that needs it).
References¶
docs/internal/snowflake-phase2-listings-2026-04-24.sql— last known-good CREATE LISTING templatedocs/internal/dump_listing_manifest.sh— canonical manifest extractordocs/internal/snowflake-resubmit-2026-04-28.sql— Trial flip + sample-view rebuild scriptdocs/reference/snowflake-naming-convention.md—HUSKY_<GEO>_<CHANNEL>_<TIER>patterndocs/strategy/2026-04-23-publishing-strategy.md— current listing-state table (revised 2026-04-28)- Snowflake docs: listing-manifest-reference (WebFetch-able)
- Provider eligibility: provider-becoming docs
- Marketplace Ops: provider-onboarding-case form
Changelog¶
- 2026-04-29 — v2 rewrite. Integrated post-2026-04-23 learnings: corrected DDL to
CREATE EXTERNAL LISTING(wasCREATE LISTING), removed obsoleteDISTRIBUTION = EXTERNALclause, replacedtrial_detailswithlimited_trial_plan: trial_time_limit:(verified 2026-04-28 against #60 + #61), added Failure Mode #1 (real-data-leak incident), Failure Mode #2 (510404 monetization gate + SG address fix), sample-view deterministic hash pattern, expanded geography schema withcoverage.states: []requirement. Skill.claude/skills/snowflake-listing-submit/now points here as SSOT. - 2026-04-23 — v1 initial draft. 14+ rejection cycles documented; schema traps table; profile + listing lifecycle diagrams; role-switching gotcha. Captured during sprint 2026-04-20→23.