Check recipe

Colorado's AI law (SB 26-189) and Claude at work

DuckDB2026-10-31Script and PDF downloadable

The short answer

Colorado's SB 26-189, signed May 14, 2026, repeals and replaces the earlier Colorado AI Act. It takes effect January 1, 2027 and applies to consequential decisions made on or after that date, employment decisions among them. A deployer that uses automated decision-making technology to materially influence such a decision must give notice before it is used, a plain-language description within 30 days after an adverse decision, a way to request personal data, correction and human review, and keep records for at least three years. We read the signed act. Legal must say whether HR use of Claude falls inside.

What the act says

Colorado's legislature passed SB 26-189, "Automated Decision-Making Technology", and the Governor signed it on May 14, 2026 (Colorado Session Laws 2026, Chapter 131). It repeals and replaces Colorado's earlier AI Act, SB 24-205. The act sets duties for a deployer that uses a "covered ADMT": automated decision-making technology "used to materially influence a consequential decision" (6-1-1701(5)), where materially influence means an output that is "a non-de minimis factor" in the decision and "affects the outcome", and not "incidental, trivial, or clerical uses" (6-1-1701(13)). Whether an HR decision made with Claude is one is for Legal to say; this guide treats HR uses as the ones to look at first (SB 26-189, Session Laws 2026, Ch. 131 (read 2026-10-31); the legislature's bill page). The table quotes each deployer duty from the act, and the record the script reads to check it.

The deployer duties in the act, and what the script reads
What the act saysThe record the script readsWhat it prints
“Prior to” using the technology in the decision, “a clear and conspicuous notice” to the consumer (6-1-1704(1)); a “prominent public notice” at points of consumer interaction meets this (6-1-1704(2))Decision log: the date a notice was sent; or the day you began keeping a public noticeA decision with no notice recorded and no public notice in place, or a notice dated after the decision
After an adverse outcome, “within thirty days after making the decision”, a “plain language description of the consequential decision and the role the covered ADMT played” (6-1-1704(3))Decision log: the outcome, and the date the description was sentAn adverse decision with no description after 30 days, or one sent later
After an adverse outcome, on request: instructions for requesting personal data and correcting “factually incorrect or materially inaccurate” data, “consistent with section 6-1-1306” (6-1-1705(1)(a)(I))Request log: the kind, the date received, the date answeredA request not answered, or answered after a deadline you set
After an adverse outcome, on request: “an opportunity for meaningful human review and reconsideration”, “to the extent commercially reasonable” (6-1-1705(1)(a)(II))Request log, if you record review requests there: the same fieldsThe same list
Records “reasonably necessary to demonstrate compliance”, kept “for not less than three years after the date of a consequential decision” (6-1-1703)Decision log: dates and id numbersA month with no record, and missing id numbers

The act says records “may include” version identifiers, changelogs and documentation of material mitigation changes (6-1-1703); it does not list them all. Counting decision records by month, as the script does, is our proxy and not the legal duty. 6-1-1705 states no deadline for answering a request; it points to 6-1-1306, which Legal should read for response timing, so the script has no default deadline: you set one, with your counsel.

Dates and enforcement, from the act. The signed act takes effect January 1, 2027, and applies to consequential decisions made on or after that date (Section 5). The attorney general's rules on the description and on consumer rights are due on or before the same day (6-1-1704(4)(b), 6-1-1705(3)), and may change what a description must say. The attorney general enforces the act exclusively, and a violation is a "deceptive trade practice" under the Colorado Consumer Protection Act (6-1-1706(1)(b), (2)(a)); the act creates no new private right of action (6-1-1706(4)). Before an enforcement action the attorney general issues a notice of violation if a cure is deemed possible, and may sue if it is not cured within sixty days; that subsection is repealed January 1, 2030 (6-1-1706(3)).

What this covers, and what it does not

This guide does not tell you whether SB 26-189 applies to any use of Claude at your company. That is a legal question for your lawyers. We read the signed act, but the script does not test whether a technology "materially influences" a decision, who counts as a consumer, who is a deployer, or the act's exceptions for entities that meet other legal obligations (6-1-1708). It sorts your own record of each use so Legal sees which to read first, and it checks the notices, requests and records you keep against the act's dates. If you are also looking at New York City's rule on hiring tools, our guide on NYC Local Law 144 and Claude in hiring does the equivalent there.

This is not legal advice and not a compliance test. A line in the output says what a log records against the 30 days or the deadline you set. It does not say the act was or was not followed.

What you will have at the end

A Python script, colorado_sb26189_claude_check.py, that reads your Claude use-case register, your log of employment decisions and your log of requests. It runs 19 input checks, then builds four lists and prints each with its filters, its row count and a SHA-256 of its rows. It also writes each list as a CSV. With --strict it exits with code 1 when an input check failed or a decision, request, record month or missing id number needs follow-up; FOR LEGAL TO CHECK uses alone do not, because they are a prompt for Legal, not a finding. The full detail is in a PDF: Download the full guide (PDF).

  • The use sort: every register row with a status. FOR LEGAL TO CHECK comes with the reason employment use that touches Colorado as recorded; SB 26-189, or with the answer that is missing. NOT AN EMPLOYMENT USE AS RECORDED and NO COLORADO PEOPLE AS RECORDED follow the register's own answers. The script never says a use is covered and never says it is exempt.
  • The decision review: every decision with a status and the reasons: no notice, a notice dated after the decision, an adverse decision with no description after 30 days, or a description sent late.
  • The request review: every request, answered or not, against the deadline you set.
  • The records list: each month of the window with its count of decision records, and the id numbers missing from each log.

The records we ran it on are generated: a seeded stand-in of 120 invented decisions, 30 invented requests and seven invented register rows. They are not a real organization's data and not a measurement of anything. We have not run the script on a real company's records.

What you will need

  • A register of your Claude uses in HR as a CSV with a header row: an id, whether the use supports an employment decision (hiring, promotion, pay, discipline, termination, another employment decision) or neither, whether it touches people in Colorado as your counsel defines that, and the date the row was created.
  • A log of employment decisions where Claude's output was used, as a CSV with a header row: a decision id, the register id of the use, the decision date, whether the outcome was adverse, the date a notice was sent, the date a plain-language description was sent, and the date the record was created. These are records about people: keep them under your own access controls. The script runs on your machine and sends nothing to Data Meaning or anyone else.
  • A log of requests for personal data, correction or human review as a CSV with a header row: an id, the kind, the date received, the date answered and the date the record was created.
  • Python 3 and the duckdb module. The script reads each CSV with DuckDB's read_csv using a header row and all_varchar (DuckDB: CSV overview (read 2026-10-31)).

Step 1. Set the settings that have no default

Three settings decide what the checks mean, and each has no default. REQUEST_DAYS is the number of days you allow for answering a request: 6-1-1705 states none, so counsel sets it. FIRST_USE_ON is the day you began using Claude in employment decisions. IDS_ARE_SEQUENTIAL says whether your ids end in a number that goes up by one: True checks for missing numbers, False (ids from a shared counter, or not numbered) skips that check. If one of the three is unset, its check fails: a check that could not run is not reported as ok. AS_OF is the day the check is for, and None means today. PUBLIC_NOTICE_SINCE is optional: the day you began keeping a prominent public notice under 6-1-1704(2); decisions on or after it need no notice of their own in the log. The same block holds the answer lists, which are ours, and you can disagree with them. The demo runs pass values on the command line (--request-days 45, --first-use-on 2026-01-01, --ids-sequential yes, --as-of 2026-10-31); 45 days is a number we chose for the demo, not one from the act.

# SETTINGS WITH NO DEFAULT: an unset one fails its check, because a check that could not run is not a pass.
REQUEST_DAYS = None       # the number of days you will allow to answer a request. 6-1-1705 states none (it points to 6-1-1306); your counsel sets it.
FIRST_USE_ON = None       # YYYY-MM-DD: the day you began using Claude in employment decisions
AS_OF = None              # YYYY-MM-DD: the day the check is for; None = today
KNOWN_GAP_MONTHS = []     # months (YYYY-MM) with no decision records that you have explained, for example ["2026-03"]
PUBLIC_NOTICE_SINCE = None  # YYYY-MM-DD: the day you began keeping a prominent public notice at points of consumer interaction.
                            # 6-1-1704(2) says that meets the notice duty; a decision on or after it with no notice of its own is
                            # then not flagged. None: you rely on notices sent per decision, and each must be in the log.
IDS_ARE_SEQUENTIAL = None  # True: ids end in a number that goes up by one (D-001, D-002); a missing number is reported. False: ids come
                           # from a shared counter or are not numbered, so gaps are not checked. None: not set, and that check fails.

Dates must be written YYYY-MM-DD and nothing else. DuckDB's own date cast reads 8/5/26 as the year 8 and accepts 2026/09/24, so the script matches the pattern first and only then parses the date. A date that is not in that form is counted and fails an input check. It is never dropped.

Step 2. Sort each use, as recorded

The sort takes the first status that applies: a decision-type answer that is empty or unknown goes to Legal; a use that supports no employment decision is set aside; a Colorado answer that is empty or unknown goes to Legal; a use recorded as touching no one in Colorado is set aside; any other use goes to Legal. The statuses say what the register records. They do not say what the act requires.

        CASE WHEN d.meaning IS NULL THEN 'FOR LEGAL TO CHECK'
             WHEN d.meaning = 'neither' THEN 'NOT AN EMPLOYMENT USE AS RECORDED'
             WHEN c.meaning IS NULL THEN 'FOR LEGAL TO CHECK'
             WHEN c.meaning = 'no' THEN 'NO COLORADO PEOPLE AS RECORDED'
             ELSE 'FOR LEGAL TO CHECK' END AS status,

Step 3. Check the notice and the 30-day description

For each decision the script finds the due date, the decision date plus 30 days, and compares the log with it. The act counts the 30 days from the decision (“within thirty days after making the decision”); the script uses the decision date. An adverse decision with no description is a hard finding once the as-of date is past the due date; before that it is shown as open, not as a fault. The act requires the notice before the technology is used in the decision (6-1-1704(1)), so a notice dated after the decision is a follow-up item, printed as NOTICE DATED n DAYS AFTER THE DECISION; no demo run plants this fault. A decision with no notice of its own is a finding unless you set PUBLIC_NOTICE_SINCE and the decision is on or after it. A blank description is a finding. An unreadable date is never guessed.

        hard, soft = [], []
        if outcome is None:
            hard.append("outcome not recorded or not recognized")
        if decided is None:
            hard.append("decision date not read")
        else:
            if not n_given and public_since is not None and decided >= public_since:
                pass                                  # 6-1-1704(2): the public notice you keep meets the duty
            elif not n_given:
                hard.append("NO NOTICE RECORDED")
            elif notice is None:
                hard.append("notice date not read")
            elif notice > decided:
                hard.append(f"NOTICE DATED {(notice - decided).days} DAYS AFTER THE DECISION")
            if outcome == "adverse":
                due = decided + datetime.timedelta(days=DESCRIPTION_DAYS)
                if not s_given:
                    (hard if as_of > due else soft).append(
                        f"NO DESCRIPTION, {DESCRIPTION_DAYS} DAYS PASSED (due {due})" if as_of > due else f"description due {due}")
                elif sent is None:
                    hard.append("description date not read")
                elif sent > due:
                    hard.append(f"DESCRIPTION SENT {(sent - decided).days} DAYS AFTER THE DECISION")

Step 4. Check each request against your deadline

A request with no answer is a finding once the as-of date passes its due date, and open before that. An answer dated after the due date is a finding that gives the days taken. An answer dated before the request was received is an input fault, because the log cannot be right.

Step 5. Look for a gap in the 3 years of records

The window starts at the later of three years before the as-of date and your FIRST_USE_ON, and ends at the as-of date. The script lists every month in it with its count of decision records. A finished month with none is flagged unless you name it in KNOWN_GAP_MONTHS: a month with no record may be real, or may be a log that lost rows, and the script cannot tell which. A month still running on the as-of date, and a part month at the window start, are shown but not counted as gaps. If IDS_ARE_SEQUENTIAL is True, it also reads each id as a prefix and a number and counts every number missing between the lowest and highest, printing them the way your log writes them (D-025, not d-25). The screen shows the first twelve and the count; records_kept.csv lists every one. Both checks look at the whole window, not one period, so one explained old gap does not fail every later check.

def id_gaps(ids, limit):
    """ids: id strings as the log writes them. Groups by prefix ignoring case, and returns (count of missing numbers,
    up to `limit` missing ids written the way the log writes them: its own prefix and zero padding)."""
    by_prefix = {}
    for i in ids:
        m = re.fullmatch(r"(.*?)(\d+)", i)
        if m:
            g = by_prefix.setdefault(m.group(1).lower(), {"shown": m.group(1), "width": len(m.group(2)), "nums": set()})
            g["nums"].add(int(m.group(2)))
    count, listed = 0, []
    for _, g in sorted(by_prefix.items()):
        nums = g["nums"]
        count += max(nums) - min(nums) + 1 - len(nums)
        k = min(nums)
        while len(listed) < limit and k <= max(nums):
            if k not in nums:
                listed.append(f"{g['shown']}{k:0{g['width']}d}")
            k += 1
    return count, listed

Step 6. Run the input checks before trusting any list

  • Settings: the as-of date, the request deadline, the first-use date and the explained months (trip on: a date such as 31/10/2026, no deadline set, no first-use date, or a month written 3/2026; run 3 shows the unset deadline).
  • Ids: register, decision and request ids listed more than once, ignoring case, and rows with no id (trip on: a duplicated or unnumbered row).
  • Answers in no list: a decision type or Colorado answer in the register, an outcome in the decision log, a kind in the request log (trip on: selection, denied, appeal; an empty answer counts).
  • A decision that names a use not on the register, including a blank use id (trips on: a decision that names UC-999).
  • Dates missing or not written YYYY-MM-DD, and dates after the as-of date (trip on: 6/12/26, or a record dated 2027-01-01).
  • A description dated before its decision, or a request answered before it was received (trips on: a request answered two days before it arrived). Run 2 plants no description dated before its decision, so no run shows that half of the check tripping.

A word in your list that no row uses is printed as information. A failed input check marks every list INCOMPLETE.

Run 1: clean generated records

Seed 226, seven invented register rows, 120 invented decisions (12 in each month from January to October 2026) and 30 invented requests. All 19 input checks pass. Four of the seven uses are for Legal. No decision has a flag, and one request is still open inside the deadline. The blocks below are excerpts of the run; the PDF has the full output.

as of     : 2026-10-31   request deadline: 45 days   first use: 2026-01-01   explained gap months: none

== Input checks   (register rows: 7; decision rows: 120; request rows: 30)
[ok  ] as-of date not written as YYYY-MM-DD: 0
[ok  ] request deadline not set, or not a whole number of days above zero: 0
[ok  ] ids-are-sequential setting not set (True or False): 0
[ok  ] public-notice date not written as YYYY-MM-DD: 0
[ok  ] first-use date not set, or not written as YYYY-MM-DD: 0
[ok  ] explained gap months not written as YYYY-MM: 0
[ok  ] register ids listed more than once (ignoring case): 0
[ok  ] register rows with no use case id: 0
[ok  ] register answers in no list (decision, Colorado): 0
[ok  ] decision ids listed more than once (ignoring case): 0
[ok  ] decision rows with no decision id: 0
[ok  ] decisions naming a use that is not on the register: 0
[ok  ] decision outcomes in no list: 0
[ok  ] request ids listed more than once (ignoring case): 0
[ok  ] request rows with no request id: 0
[ok  ] request kinds in no list: 0
[ok  ] dates missing or not written as YYYY-MM-DD (a blank notice or description is not counted: it is a finding): 0
[ok  ] dates after the as-of date: 0
[ok  ] descriptions dated before their decision, or answers before their request: 0
[info] decision answers in your word list that no row uses: discipline; other employment decision; termination
== SB 26-189: the use sort: use_sort
filters   : every register row, none dropped; first applicable status; an empty or unrecognized answer is FOR LEGAL TO CHECK, never outside
rows      : 7
sha256    : 8c7982900c4a567874a33a838bb20dd2780c6e0c83fbb50fc8e57be5e9b2429c
  1 | UC-201 | FOR LEGAL TO CHECK | employment use that touches Colorado as recorded; SB 26-189
  2 | UC-202 | FOR LEGAL TO CHECK | employment use that touches Colorado as recorded; SB 26-189
  3 | UC-203 | NOT AN EMPLOYMENT USE AS RECORDED | 
  4 | UC-204 | FOR LEGAL TO CHECK | employment use that touches Colorado as recorded; SB 26-189
  5 | UC-205 | NOT AN EMPLOYMENT USE AS RECORDED | 
  6 | UC-206 | FOR LEGAL TO CHECK | employment use that touches Colorado as recorded; SB 26-189
  7 | UC-207 | NO COLORADO PEOPLE AS RECORDED | 
  FOR LEGAL TO CHECK: 4; NO COLORADO PEOPLE AS RECORDED: 1; NOT AN EMPLOYMENT USE AS RECORDED: 2
  This sorts uses by what the register says. It does not say the act reaches a use or does not: Legal decides.
== SB 26-189: notice and the 30-day description: decision_review
filters   : every decision row; notice before the decision (6-1-1704(1)); public notice kept since: NOT SET (each decision needs its own); adverse = outcome mapped to adverse; description due 30 days after the decision date (6-1-1704(3)); a blank description is a finding, an unreadable date is not guessed
rows      : 120
sha256    : 80291a70a20fa13e3596cc12f76bf6c60f20715c7a28e040b2e2c77d3c0ce047
  NO FLAG: 120
  A flag says what the log records against the 30 days and the notice. It does not say the act was or was not followed.

== SB 26-189: requests answered: request_review
filters   : every request row; deadline = 45 days from receipt, a number YOU set (6-1-1705 states none); a blank answer date is a finding
rows      : 30
sha256    : e44fc29dd127aa99165e40ffb8d7787bf5e9b74abcb81cac29642cec29cc6796
  30 | R-030 | OPEN, INSIDE DEADLINE | open, due 2026-11-28
  NO FLAG: 29; OPEN, INSIDE DEADLINE: 1

== SB 26-189: records kept for at least 3 years: records_kept
filters   : window 2026-01-01 to 2026-10-31 (the later of 3 years before the as-of date and your first-use date); one record needed in each month unless explained (a month still running on the as-of date, or a part month at the window start, is not a gap); id numbers checked for gaps
rows      : 13
sha256    : 9451d515206a2a9d357323a449b5f1302e479652e585a7127c4bb90619cf1d44
  months in the window: 10; months with no record: 0
  A month with no record may be real (no decisions) or a deleted log: the script cannot tell which. Explain it in KNOWN_GAP_MONTHS or find the records.

[info] decisions dated before 2027-01-01, when the act begins to apply (Section 5(3)): 120 of 120

== Result
input checks : all passed
for Legal    : 4 use(s) marked FOR LEGAL TO CHECK
decisions    : 0 for follow-up, 0 open inside 30 days
requests     : 0 for follow-up, 1 open inside the deadline
records      : 0 month(s) with no decision record, 0 missing decision id(s), 0 missing request id(s)
This is not legal advice and not a compliance test. Legal decides what the act requires.

Run 2: planted faults, and the script finds them

The same script with --plant. It plants a fault for each input check that reads the files, except that no description is dated before its decision: a duplicated and a missing id in each of the three files, a decision that names a use not on the register, an outcome and a request kind in no list, a date written 6/12/26, a record dated 2027-01-01 and a request answered before it was received. It also plants an adverse decision with no description after 30 days and one sent on day 41, a decision with no notice, a request never answered and one answered after 60 days, every decision in March 2026 removed, and one more row removed. It does not plant a notice dated after its decision. Each fault trips its check, and every list is marked INCOMPLETE.

== Input checks   (register rows: 12; decision rows: 109; request rows: 32)
[ok  ] as-of date not written as YYYY-MM-DD: 0
[ok  ] request deadline not set, or not a whole number of days above zero: 0
[ok  ] ids-are-sequential setting not set (True or False): 0
[ok  ] public-notice date not written as YYYY-MM-DD: 0
[ok  ] first-use date not set, or not written as YYYY-MM-DD: 0
[ok  ] explained gap months not written as YYYY-MM: 0
[FAIL] register ids listed more than once (ignoring case): 1   (trips on: a duplicated register row)
[FAIL] register rows with no use case id: 1   (trips on: a register row with no id)
[FAIL] register answers in no list (decision, Colorado): 4   (trips on: an answer such as 'selection' that is not one of your words (an empty answer counts))
[FAIL] decision ids listed more than once (ignoring case): 1   (trips on: a duplicated decision row)
[FAIL] decision rows with no decision id: 1   (trips on: a decision row with no id)
[FAIL] decisions naming a use that is not on the register: 1   (trips on: a decision that names UC-999)
[FAIL] decision outcomes in no list: 1   (trips on: an outcome such as 'denied')
[FAIL] request ids listed more than once (ignoring case): 1   (trips on: a duplicated request row)
[FAIL] request rows with no request id: 1   (trips on: a request row with no id)
[FAIL] request kinds in no list: 1   (trips on: a kind such as 'appeal')
[FAIL] dates missing or not written as YYYY-MM-DD (a blank notice or description is not counted: it is a finding): 1   (trips on: a date such as 6/12/26, or a decision with no date)
[FAIL] dates after the as-of date: 1   (trips on: a record dated 2027-01-01)
[FAIL] descriptions dated before their decision, or answers before their request: 1   (trips on: a request answered two days before it was received)
[info] decision answers in your word list that no row uses: other employment decision; termination
== SB 26-189: notice and the 30-day description: decision_review   ** INCOMPLETE: see input checks **
filters   : every decision row; notice before the decision (6-1-1704(1)); public notice kept since: NOT SET (each decision needs its own); adverse = outcome mapped to adverse; description due 30 days after the decision date (6-1-1704(3)); a blank description is a finding, an unreadable date is not guessed
rows      : 109
sha256    : 28c31ef1535cdf78fe531375c0ec3a227f92eff48fd98119fa873ce26a994529
  38 | D-050 | FOR FOLLOW-UP | NO NOTICE RECORDED
  39 | D-051 | FOR FOLLOW-UP | NO DESCRIPTION, 30 DAYS PASSED (due 2026-06-05)
  40 | D-052 | FOR FOLLOW-UP | DESCRIPTION SENT 41 DAYS AFTER THE DECISION
  41 | D-053 | FOR FOLLOW-UP | decision date not read
  42 | D-054 | FOR FOLLOW-UP | outcome not recorded or not recognized
  FOR FOLLOW-UP: 5; NO FLAG: 104
  A flag says what the log records against the 30 days and the notice. It does not say the act was or was not followed.

== SB 26-189: requests answered: request_review   ** INCOMPLETE: see input checks **
filters   : every request row; deadline = 45 days from receipt, a number YOU set (6-1-1705 states none); a blank answer date is a finding
rows      : 32
sha256    : 7e2b7315095fdef72311b69f63f5253f8ddd96177e3aa36cadb9fd94b2c8c79d
  4 | R-004 | FOR FOLLOW-UP | NOT ANSWERED, DEADLINE PASSED (due 2026-04-21)
  6 | R-006 | FOR FOLLOW-UP | ANSWERED 60 DAYS AFTER RECEIPT (deadline 45)
  10 | R-010 | FOR FOLLOW-UP | kind not recorded or not recognized
  30 | R-030 | OPEN, INSIDE DEADLINE | open, due 2026-11-28
  FOR FOLLOW-UP: 3; NO FLAG: 28; OPEN, INSIDE DEADLINE: 1

== SB 26-189: records kept for at least 3 years: records_kept   ** INCOMPLETE: see input checks **
filters   : window 2026-01-01 to 2026-10-31 (the later of 3 years before the as-of date and your first-use date); one record needed in each month unless explained (a month still running on the as-of date, or a part month at the window start, is not a gap); id numbers checked for gaps
rows      : 13
sha256    : 08003732da528a9e79577ef4fd2542da6d4bc85e8c1cd311cf4745e11c431ad7
  month | 2026-03 | 0 | NO RECORDS IN THIS MONTH
  decision ids | 108 ids read | 13 | MISSING: D-025, D-026, D-027, D-028, D-029, D-030, D-031, D-032, D-033, D-034, D-035, D-036 ... (13 in all; every one is in records_kept.csv)
  months in the window: 10; months with no record: 1
  A month with no record may be real (no decisions) or a deleted log: the script cannot tell which. Explain it in KNOWN_GAP_MONTHS or find the records.

[info] decisions dated before 2027-01-01, when the act begins to apply (Section 5(3)): 108 of 109

== Result
input checks : FAILED, the lists above are not complete
for Legal    : 9 use(s) marked FOR LEGAL TO CHECK
decisions    : 5 for follow-up, 0 open inside 30 days
requests     : 3 for follow-up, 1 open inside the deadline
records      : 1 month(s) with no decision record, 13 missing decision id(s), 0 missing request id(s)
This is not legal advice and not a compliance test. Legal decides what the act requires.

March 2026 has no record and 13 id numbers are missing: the 12 that were March's decisions and one more. The decision numbered D-053 has the date 6/12/26, so the script says "decision date not read" and does not guess. The fault planted in D-054 is an outcome of denied, which is in no list. The demo dates fall before January 1, 2027, so no duty applied on them; the script counts such decisions on an [info] line. These findings come from invented records and say nothing about any real company.

Run 3: no request deadline set

The same clean records, with no --request-days and REQUEST_DAYS left as None. The check for the deadline fails, and every request is for follow-up because there is nothing to measure it against. The script does not fill in a number for you.

[FAIL] request deadline not set, or not a whole number of days above zero: 1   (trips on: no --request-days and REQUEST_DAYS left as None; 6-1-1705 states no deadline, so you set one)
== SB 26-189: requests answered: request_review   ** INCOMPLETE: see input checks **
filters   : every request row; deadline = NOT SET from receipt, a number YOU set (6-1-1705 states none); a blank answer date is a finding
rows      : 30
sha256    : 3c12b8ecd040e4b92a7b02a1b5856f0fbf5a3b405e3d4e2d17502c4e4516fd9f
  1 | R-001 | FOR FOLLOW-UP | DEADLINE NOT SET
  2 | R-002 | FOR FOLLOW-UP | DEADLINE NOT SET
== Result
input checks : FAILED, the lists above are not complete
for Legal    : 4 use(s) marked FOR LEGAL TO CHECK
decisions    : 0 for follow-up, 0 open inside 30 days
requests     : 30 for follow-up, 0 open inside the deadline
records      : 0 month(s) with no decision record, 0 missing decision id(s), 0 missing request id(s)
This is not legal advice and not a compliance test. Legal decides what the act requires.

How to read the results

  • A status is a prompt for Legal, not a finding. FOR LEGAL TO CHECK means the register says the use is an employment use that touches Colorado, or the register is silent or not recognized.
  • FOR FOLLOW-UP means the log records something against the 30 days, the notice or your deadline. It does not say the act was breached: the log may be wrong, and the act is the authority.
  • OPEN is not a fault. It means the due date has not yet passed on the as-of date.
  • A month with no record is a question. Find the records, or write the month in KNOWN_GAP_MONTHS with your reason.
  • An INCOMPLETE list is not a result. Fix the failed input checks first.

Run it on your own data

To run it on your own data, change only the settings between the lines marked CHANGE THESE FOR YOUR OWN DATA. Set REGISTER_FILE, DECISIONS_FILE and REQUESTS_FILE to the paths of your three CSV files. In REG_COLS, DEC_COLS and REQ_COLS, put the name of the column in your files that holds each field. In DECISION_WORDS, COLORADO_WORDS, OUTCOME_WORDS and KIND_WORDS, put the answers your files use, in lower case, each with the meaning the script gives it. Set REQUEST_DAYS to the number of days your counsel allows for answering a request, FIRST_USE_ON to the day you began using Claude in employment decisions, written YYYY-MM-DD, and IDS_ARE_SEQUENTIAL to True if your ids end in a number that goes up by one, or to False if they do not. If you keep a prominent public notice at points of consumer interaction, set PUBLIC_NOTICE_SINCE to the day you began, written YYYY-MM-DD. Leave AS_OF as None to check as of today, or set it to a date written YYYY-MM-DD. An answer that is in none of those lists is counted and fails an input check, so the run tells you which one to add.

We also ran the script on a second set of generated files with other file names, other column names, other answer words, upper-case answers, a blank Colorado answer and a blank use id, and a fault planted for each input check that reads the files, except a description dated before its decision. It read RECRUITMENT and Y as an employment use that touches Colorado, sent the blank Colorado answer to Legal, and failed the checks for the repeated ids (including hr-5 beside HR-5), the rows with no id, the decision that names a use not on the register, the date after the as-of date, and the answers in no list. It flagged a decision with no notice, an adverse decision with no description after 30 days, a description sent on day 48, a request never answered, a month with no record, and the missing id numbers, printed as the log writes them (CASE-09). With PUBLIC_NOTICE_SINCE set, it did not flag a later decision that had no notice of its own; with the setting removed, it did. With the as-of date in the middle of a month that had no records yet, it showed that month as MONTH NOT OVER, NO RECORDS YET, and with the first-use date in the middle of a month that had none, it showed that month as PART MONTH AT WINDOW START, NO RECORDS; it counted neither as a gap. We have not run it on a real company's records.

Download colorado_sb26189_claude_check.py
Download the full guide (PDF)

Method

  • Input: generated records, written by the script with a fixed seed (226): seven invented register rows, 120 invented decisions and 30 invented requests. The outcomes, dates and delays are invented for the demonstration.
  • The law's text: the duties, the 30 days, the 3 years, the dates and the enforcement points are quoted from the signed act as published in the 2026 Session Laws, Chapter 131 (read 2026-10-31), with section numbers. The legislature's site refuses scripted downloads, so the script does not fetch the act and nothing on this page is pinned.
  • The 30 days: the act counts the 30 days from the decision (“within thirty days after making the decision”); the script uses the decision date, so a description sent on the 30th day is not flagged.
  • The window: the later of three years before the as-of date and the first-use date you give, to the as-of date.
  • Settings you can disagree with are at the top of the script: the answer lists, the column names, the request deadline, the first-use date, whether ids are sequential and the public-notice date.
  • Every problem row is counted. An unrecognized answer or date is counted and never guessed. A failed input check marks the lists INCOMPLETE.

Limits

  • This is not a client result. The records are generated. The runs show the script finds planted faults. They say nothing about any real company.
  • This is not legal advice, and the script does not decide whether SB 26-189 reaches a use. It sorts uses by what your register says. Legal decides.
  • We quote the act; we do not apply all of it. The script does not test the definitions (covered ADMT, materially influence, consumer, deployer) or the act's provisions on compliance with other legal obligations, insurers and covered entities (6-1-1708). Legal applies them.
  • The rules are still to come. The signed act takes effect January 1, 2027, and applies to consequential decisions made on or after that date (Section 5). The attorney general's rules on the description and on consumer rights are due on or before the same day (6-1-1704(4)(b), 6-1-1705(3)) and may change what a description must say. Check the attorney general's page for the current position.
  • The request deadline is yours. 6-1-1705 states no deadline; it points to 6-1-1306, which Legal should read for response timing. 45 days in the demo is a number we chose.
  • Our reading is not Colorado's. The monthly record test, the id-gap test and the mapping of your answers are ours. We have not asked the attorney general to review them.
  • A log can only show what it holds. A decision never entered in the log cannot be checked, and a month with no record may be a quiet month.
  • The logs hold personal data. Keep them under your own controls and out of the PDF, the CSVs and anything you share.

Tested on

Tested on

Platform
DuckDB 1.5.6, Python 3.14, run from Python with the duckdb module
Edition
Open-source DuckDB, in-memory
Date
2026-10-31
Dataset
Generated records, seed 226 (not a real organization). Duties and dates from the signed act, Session Laws 2026, Ch. 131 (read 2026-10-31)
Code
colorado_sb26189_claude_check.py

When this is not enough

  • You need an answer on whether the act reaches a use. Take the FOR LEGAL TO CHECK rows, and the rows whose answers you doubt, to your counsel with the signed act.
  • You do not yet keep these logs. A notice, a description and a request have to be recorded with dates before this script has anything to check.
  • You need to know what the technology did in each decision. The script checks dates and ids. It does not check what a description said or whether a human review was meaningful.
  • The attorney general's rules arrive. Re-read what a description must contain, and change the settings and your logs to match.

Run Claude in your own Azure subscription

Control Tower for Claude runs in your own Azure subscription. See the solution page.

See Control Tower for Claude