Automation scripts

A script is one of the two kinds of action an automation rule can take. When the rule fires, the script runs once, with the data of the event that triggered it, and calls platform functions directly. Scripts suit fixed, predictable steps — award an achievement, move a member into a group, set a medal — that should run the same way every time.

The language

Scripts are written in Starlark, a small deterministic dialect of Python. Assignment, if/elif/else, for, lists, dicts, tuples, comprehensions and function definitions all work as they do in Python.

A script cannot open files, make network requests, or import anything. It can read the event data and call the functions on this page. Both are predeclared, so a script sees them without any import.

The script is checked for syntax when you save the rule; it is not run at that point.

Limits

Limit

Value

Running time

10 seconds

Execution steps

1,000,000

The deadline is what actually bounds a run. The step limit is a backstop against a runaway loop — walking a contest scoreboard costs a few hundred steps per row, so it sits far above what a real rule needs. Exceeding either limit stops the script, and the error is written to the rule's log if the run is being logged.

What the environment disables

Beyond the usual Starlark restrictions, several optional features are turned off:

Restriction

What to do instead

No while loops

Iterate with for.

No recursion

A function cannot call itself.

No set() type

Use a list or a dict.

No global rebinding

Keep counters and accumulators inside a function.

if and for are allowed at the top level.

Global rebinding is the one that catches people out. A top-level name may be bound once; a second assignment to the same top-level name — including one inside the body of a top-level for — is a compile error, cannot reassign global …. A local variable inside a function has no such restriction:

def count_official(participants):
    n = 0
    for p in participants:
        if not p.unofficial:
            n = n + 1   # local, fine
    return n

total, participants = eolymp_participants_list(contest_id = contest.id, size = 500)
printf("official entrants: %d", count_official(participants))

Reading the event data

The variables a script receives depend on the rule's trigger; Triggers and conditions lists which. Each is an object whose fields you read with a dot — submission.verdict, member.attributes.grade, contest.problem_count. Timestamps are RFC 3339 text.

Functions

All functions take keyword arguments. Arguments marked (optional) may be omitted. Reading functions return a value; writing functions return nothing unless noted.

Achievements

eolymp_achievements_assign(member_id, achievement_id, qty=, inc_by=, reference=)

Grant an achievement. The count is set to 1 by default; qty sets it to an exact number and inc_by increases it. reference is a deduplication key — repeated runs carrying the same reference apply once.

Credits

eolymp_credits_grant(member_id, amount, reference, note=, expires_at=)

Grant credits to a member. amount must be positive. reference is required and makes the grant unique per member: the first grant returns True, and a second grant with the same reference returns False instead of paying out again. expires_at is an RFC 3339 timestamp.

Emails

eolymp_emails_send(member_id, template, params=, locale=, reference=, email_type=)

Send a templated email to a member. template is the path of the email template and params is a dict of values passed to it. A non-empty reference makes the send permanently unique per member — that member is never emailed twice under the same reference — and an empty reference switches that off. email_type defaults to a general, quota-counted, unsubscribable type; the account and security type is reserved and is rejected.

Members

eolymp_members_get(id)                                                  # → member
eolymp_members_list(filters=, search=, size=, offset=, sort=, order=)    # → (total, [member])
eolymp_members_set_attributes(member_id, attributes)
eolymp_members_set_preferences(member_id, locale=, timezone=, runtime=)
eolymp_members_add_group(member_id, group_id)
eolymp_members_remove_group(member_id, group_id)
eolymp_members_set_active(member_id, active)

attributes is a dict of attribute keys to text or whole numbers, merged onto the member's existing attributes; keys you do not list are untouched, and passing an empty dict does nothing. set_preferences changes only the fields you pass, and runtime is the member's default solution language.

Groups

eolymp_groups_get(id)                          # → group
eolymp_groups_list(filters=, size=, offset=)   # → (total, [group])

Participants

eolymp_participants_get(contest_id, participant_id)   # → participant
eolymp_participants_list(contest_id, filters=, search=, size=, offset=, sort=, order=)   # → (total, [participant])
eolymp_participants_set_official(contest_id, participant_id, official)
eolymp_participants_set_medal(contest_id, participant_id, medal)
eolymp_participants_set_extra_time(contest_id, participant_id, seconds)
eolymp_participants_set_active(contest_id, participant_id, active)
eolymp_participants_disqualify(contest_id, participant_id, requalify=, reason=)

medal accepts "gold", "silver", "bronze", "honorable_mention" or "none"; anything else is an error rather than a silent no-medal. disqualify takes requalify = True to reverse itself, and reason is explanatory text stored with the disqualification.

Contests

eolymp_contests_get(id)                                    # → contest
eolymp_contests_list(filters=, search=, size=, offset=)     # → (total, [contest])
eolymp_contests_get_submission(contest_id, id)              # → contest submission
eolymp_contests_list_submissions(contest_id, filters=, after=, size=, offset=)   # → (total, [contest submission])

Scoreboard

eolymp_scoreboard_list_rows(contest_id, mode=, filters=, size=, offset=, sort=, order=)   # → (total, [row])

Read a contest's standings. mode is "main" — the default, the final standing — or "frozen", "upsolve" or "virtual"; an unrecognised mode is an error. A participant's own score says nothing about placement, so a rule handing out medals or prizes works from here.

Problems

eolymp_problems_get(id)                                                  # → problem
eolymp_problems_list(filters=, search=, size=, offset=, sort=, order=)    # → (total, [problem])

Submissions

eolymp_submissions_get(id)                                    # → submission
eolymp_submissions_list(filters=, after=, size=, offset=)      # → (total, [submission])
eolymp_submissions_aggregate(group_by=, filters=, metric=, range_start=, range_end=)   # → [bucket]

group_by takes one or more dimensions — "SUBMITTED_AT", "VERDICT", "STATUS" — and an unknown name is an error. metric defaults to "COUNT". Without range_start and range_end only the last 30 days are counted. Each bucket has dimensions and count.

Pages

eolymp_pages_get(id)   # → page

Time

Timestamps are RFC 3339 text and Starlark has no clock or duration type, so time arithmetic goes through these.

time_diff(a, b)                    # → a − b, in whole seconds; negative when a is earlier
time_shift(timestamp, seconds)     # → RFC 3339 string
convert_time(timestamp, timezone)  # → (local RFC 3339 string, UTC offset such as "+02:00")

timezone is an IANA name, for example "Europe/Kyiv". An empty or malformed timestamp is an error, not a zero date.

Random

random_int(min, max)     # inclusive at both ends
random_choice(seq)
random_sample(seq, k)    # k distinct elements, shuffled
random_shuffle(seq)

Every value a script sees is identical on every run, so a hand-rolled shuffle would produce a fixed permutation. These exist for raffles, prize draws and sampling. random_sample asked for more elements than the sequence holds returns all of them shuffled rather than failing, and passing a string where a list is expected is an error. The generator is not suitable for tokens or keys.

Logging

printf(format, *args)   # %s text, %d whole numbers, %v any value
print(*args)

Both append a message to the rule's log. They produce nothing unless Debug or Dry run is on for that run.

Lists and filters

Every *_list function returns two values — the total number of matches and the current page:

total, members = eolymp_members_list(size = 5)
printf("space has %d members", total)
for m in members:
    printf("- %s", m.display_name)

size and offset page through results. search, sort and order ("asc" or "desc") are available where the underlying list supports them.

filters is a dict keyed by field name. Each field maps either to a bare value, meaning equality, or to a dict of operator to value:

{"verdict": "ACCEPTED"}                         # equals
{"level": {"gte": 5}}                           # greater than or equal
{"display_name": {"contains": "team"}}          # substring
{"created_at": {"gt": "2026-01-01T00:00:00Z"}}  # after a date

Operator

Meaning

eq

equals (the default)

ne, neq

not equal

gt

greater than

ge, gte

greater than or equal

lt

less than

le, lte

less than or equal

contains, containing

contains substring

starts, starting, prefix

starts with

Values may be text, whole numbers, True or False, or an RFC 3339 timestamp string. An unknown field name or operator stops the script with an error.

What a script does in a dry run

Reading functions work normally. The first writing function is recorded in the log as Dry run, with the arguments it would have used, and then raises — which stops the script there. A dry run therefore shows what the first write would have been, not the whole sequence of writes a real run would perform.

Objects

The fields available on each object, whether it arrives as event data or comes back from a function.

Object

Fields

submission

id, problem_id, user_id, member_id, lang, runtime, status, verdict, score, cost, percentage, submitted_at, judged_at, time_usage, cpu_usage, memory_usage, resource_usage

contest submission

id, contest_id, problem_id, participant_id, lang, runtime, status, verdict, score, cost, percentage, deleted, submitted_at, time_usage, cpu_usage, memory_usage, resource_usage

member

id, external_ref, display_name, rank (number), rating (number), level, inactive, attributes, user_name, user_nickname, user_email, user_email_verified, user_picture, user_city, user_country

contest

id, key, slug, name, url, image_url, format, series, classification, status, visibility, participation_mode, duration (seconds), starts_at, ends_at, problem_count, participant_count, allow_upsolve, require_admission, rated

participant

id, member_id, display_name, role, status, unofficial, ghost, inactive, disqualified, finalized, started_at, end_at

row

id, member_id, rank, rank_length, rank_all, rank_all_length, score, penalty, unofficial, disqualified, medal

score

score, penalty, solved, upsolve, breakdown

ticket

id, contest_id, member_id, participant_id, subject, message, status, reply_count, created_at, updated_at, last_reply_at

reply

id, ticket_id, author (participant or jury), member_id, user_id, message, created_at

problem

id, url, type, number, title, language, languages, topics, difficulty, score, acceptance_rate, submissions_count, submissions_accepted, author, source, origin, visible, time_limit, time_limit_min, time_limit_max, cpu_limit, cpu_limit_min, cpu_limit_max, memory_limit, memory_limit_min, memory_limit_max

group

id, name, description, external_ref, icon, badge, color

page

id, path, locale, locales, title, draft, automatic, content, labels

space

id, key, name, url, status

A few of those fields need unpacking:

  • member attributes is an object keyed by attribute key, read as member.attributes.grade.

  • contest classification is an object with year, series, scale, difficulty, country, region and city.

  • contest submission problem_id is the contest's problem, not the archive problem.

  • row is a scoreboard row, and its id is the participant id. It is also the row variable of the Participant finalized trigger.

  • score breakdown is a list, each item with problem_id, solved, score, percentage and attempts.

  • submission lang is the short language, such as cpp, while runtime is the full runtime id.

Examples

Grant an achievement for an accepted submission, once per problem — trigger Submission completed:

if submission.verdict == "ACCEPTED":
    eolymp_achievements_assign(
        member_id = submission.member_id,
        achievement_id = "<achievement-id>",
        reference = submission.problem_id,
    )

Award medals from the final standings — trigger Contest action, run after the contest ends and before finalizing it:

total, rows = eolymp_scoreboard_list_rows(contest_id = contest.id, mode = "main", size = 500)

def medal_for(rank):
    if rank <= 1:
        return "gold"
    elif rank <= 3:
        return "silver"
    elif rank <= 6:
        return "bronze"
    return ""

for r in rows:
    if r.unofficial or r.disqualified:
        continue
    m = medal_for(r.rank)
    if m:
        eolymp_participants_set_medal(contest_id = contest.id, participant_id = r.id, medal = m)
        printf("%s → %s", r.id, m)

Route new members into a group by attribute — trigger Member changed:

if member.attributes.grade > 9:
    eolymp_members_add_group(member_id = member.id, group_id = "<senior-group-id>")
else:
    eolymp_members_add_group(member_id = member.id, group_id = "<junior-group-id>")