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.
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.
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.
Beyond the usual Starlark restrictions, several optional features are turned off:
Restriction | What to do instead |
|---|---|
No | Iterate with |
No recursion | A function cannot call itself. |
No | 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))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.
All functions take keyword arguments. Arguments marked (optional) may be omitted. Reading functions return a value; writing functions return nothing unless noted.
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.
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.
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.
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.
eolymp_groups_get(id) # → group eolymp_groups_list(filters=, size=, offset=) # → (total, [group])
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.
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])
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.
eolymp_problems_get(id) # → problem eolymp_problems_list(filters=, search=, size=, offset=, sort=, order=) # → (total, [problem])
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.
eolymp_pages_get(id) # → page
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_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.
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.
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 dateOperator | Meaning |
|---|---|
| equals (the default) |
| not equal |
| greater than |
| greater than or equal |
| less than |
| less than or equal |
| contains substring |
| 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.
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.
The fields available on each object, whether it arrives as event data or comes back from a function.
Object | Fields |
|---|---|
submission |
|
contest submission |
|
member |
|
contest |
|
participant |
|
row |
|
score |
|
ticket |
|
reply |
|
problem |
|
group |
|
page |
|
space |
|
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.
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>")