Skip to content

Job data reference

This page is the full schema reference for job data directories. If you are new to job authoring, start with examples/jobs-cookbook.md for step-by-step recipes, then come back here when you need the exact attribute list.


Each job lives in its own directory under resources/jobs/:

resources/jobs/<id>/
job.xml required -- identity, title, description, eligibility
effects.xml optional -- stat/skill/brothel changes applied each shift
performance.xml optional -- how the performance score is calculated
wage.xml optional -- gold earned as a function of performance
gains.xml optional -- skill XP and trait grants each shift
messages/
work.xml optional -- shift-end message variants
refuses.xml optional -- refusal-arm message variants
banks.xml optional -- named fragment banks for composed messages
text/
en.xml optional -- localizable strings for the messages above

Only job.xml is required. A job with no other files is valid: the girl shows up in the job list, can be assigned, but nothing happens each shift.

The directory name (<id>) must be lowercase, no spaces, and must match the id attribute in job.xml. It also becomes the prefix for all text IDs in messages/work.xml and text/en.xml.


Declares the job’s identity and optional eligibility gate.

<?xml version="1.0" encoding="UTF-8"?>
<Job id="barmaid" schema="1">
<Title>Barmaid</Title>
<Description>She will staff the bar and serve drinks.</Description>
<DefaultImage>profile</DefaultImage>
</Job>
Element / attribute Required Notes
id (attribute on <Job>) Yes Must match the directory name. Lowercase, no spaces.
schema="1" (attribute on <Job>) Yes Always 1 for now.
<Title> Yes Displayed in the job picker UI.
<Description> Yes Short tooltip shown in job details.
<DefaultImage> No Image catalog type used for the shift-event portrait. Defaults to profile if omitted. Common values: profile, sex, strip, oral, wait, cook, massage, escort, dom. Individual outcomes can override this per-<Text> via the Image= attribute (see messages/work.xml below).
<Eligibility> No A block of <When> leaves. Leaves are direct children of <Eligibility> (do not wrap them in <When> here; the loader iterates the children directly, unlike the message-bank <When> surfaces where the wrapper is required). If present, a girl can only be assigned this job while every child leaf is satisfied; the same gate is re-checked at the top of every shift, so a girl whose state flipped after assignment (stock drained, status changed) produces a “could not work” event instead of running the shift. See reference/when-conditions.md for the full leaf catalogue.
<Filter> No Restricts the job to buildings that have the named feature. Text content; current values: Bar. Omit (or leave empty) for brothel-core jobs that any building hosts.
<Phase> No Selects the turn-loop pass this job runs in. Text content; one of Prepare, Produce, Main (default), Late. Use Prepare for jobs that must run before customer traffic (Advertising), Produce for jobs that publish a value other jobs read this turn (BarCook publishing food quality before BarMaid serves it), Main for everything else, Late for clean-up passes that run after the rest of the turn.

Example with eligibility:

<Job id="basictraining" schema="1">
<Title>Basic Training</Title>
<Description>She trains her combat skills.</Description>
<DefaultImage>profile</DefaultImage>
<Eligibility>
<Stat name="Level" le="10"/>
</Eligibility>
</Job>

Example with <Filter> and <Phase>:

<Job id="barcook" schema="1">
<Title>Bar Cook</Title>
<Description>She will cook food for the bar.</Description>
<DefaultImage>profile</DefaultImage>
<Filter>Bar</Filter>
<Phase>Produce</Phase>
</Job>

This job is offered only in buildings that have a Bar, and runs in the Produce pass so that any Main-pass consumer (BarMaid, BarWaitress) sees the value it publishes for the rest of the turn.


Applied once per shift for every girl working this job. Can change the girl’s stats and skills, fan out changes to all girls in the building, or change building properties.

<?xml version="1.0" encoding="UTF-8"?>
<Effects>
<SetStat target="self" stat="Tiredness" delta="-25"/>
<SetStat target="self" stat="Happiness" delta="10"/>
</Effects>

Changes a girl’s stat by a fixed or random amount.

Attribute Required Notes
target Yes Who is affected. See targets below.
stat Yes Stat name. Same set as <When><Stat>.
delta Yes (or delta_min+delta_max) Fixed integer change. Negative values decrease the stat.
delta_min / delta_max Yes (or delta) Random range, inclusive. The engine picks uniformly.
clamp_max No Cap the resulting value at this ceiling. Useful for Fame, Health, etc.

Changes a girl’s skill by a fixed or random amount.

<SetSkill target="self" skill="NormalSex" delta_min="2" delta_max="4"/>

Same attributes as <SetStat> with skill instead of stat. Skill names: Anal, Magic, BDSM, NormalSex, Beastiality, Group, Lesbian, Service, Strip, Combat, Performance.

Changes a property on the building.

<SetBrothel target="brothel" key="Filthiness" delta="-5"/>
<SetBrothel target="player.brothels" key="Fame" delta="$delta" clamp_max="100"/>
Attribute Notes
target brothel for the current building; player.brothels to fan out to every building the player owns.
key Building property to change. Current set: Filthiness, Fame.
delta Fixed integer, or $bindname to reference a <Bind> value (see below).
clamp_max Optional ceiling.

Groups a set of effects behind a <When> condition and optional <Bind> declarations. If the <When> is not satisfied, the entire group is skipped.

<EffectGroup>
<Bind name="delta" expr="(Charisma + Intelligence + Service) / 60"/>
<When>
<Bind name="delta" ge="1"/>
</When>
<SetBrothel target="player.brothels" key="Fame" delta="$delta" clamp_max="100"/>
<SetStat target="self" stat="Happiness" delta="1"/>
</EffectGroup>

Order inside a group: <Bind> elements are evaluated first, then <When> is checked using those binds, then the remaining entries are applied if the check passed.

The short alias <Group> is accepted by the engine in addition to <EffectGroup>.

Declares a named integer computed from a formula. Only valid inside an <EffectGroup>. The result can be referenced in that group’s <When> as <Bind name="..." op="..." value="N"/> and in delta="$name" on effect entries.

<Bind name="bonus" expr="clamp((Intelligence + Service) / 2 / 33, 0, 3)"/>

Supported identifiers in expr: stat names, skill names, and the clamp(value, min, max) function. You cannot reference a bind from another group; repeat the formula if you need it in two places.

Picks exactly one child <Option> per shift, weighted.

<RandomChoice>
<Option weight="6"><SetStat target="self" stat="Exp" delta="0"/></Option>
<Option weight="2"><SetSkill target="self" skill="Service" delta="1"/></Option>
<Option weight="2"><SetSkill target="self" skill="Strip" delta="1"/></Option>
</RandomChoice>

Weights are relative. In the example above: 60% no gain, 20% Service +1, 20% Strip +1.

Target string Meaning
self The girl working the job this shift.
brothel.girls Every girl currently assigned to the same building (including self).
player.brothels All buildings the player currently owns (for <SetBrothel> only).
brothel The current building (for <SetBrothel> only).

Defines how the girl’s performance score is calculated for this shift. The score is a weighted sum of her stats and skills, modified by traits.

<?xml version="1.0" encoding="UTF-8"?>
<Performance>
<Factor skill="Service" weight="3"/>
<Factor stat="Intelligence" weight="3"/>
<Factor stat="Charisma" weight="2"/>
<Factor skill="Performance" weight="2"/>
<TraitMod trait="Psychic" delta="10"/>
<TraitMod trait="Fleet of Foot" delta="10"/>
<TraitMod trait="Cum Addict" delta="-5"/>
</Performance>

Contributes one stat or skill to the performance total.

Attribute Required Notes
stat or skill Yes (one) The stat or skill to pull from.
weight Yes Multiplier. Higher = more influence. No maximum.
<When> child No If present, this factor only applies when the condition is satisfied.

The engine multiplies the girl’s value by the weight for each factor, sums all factors, and divides by the total weight. The result is a number in roughly 0-1000+ range.

Flat bonus or penalty added to the score when the girl has the named trait.

Attribute Notes
trait Trait name, case-sensitive.
delta Integer. Positive = bonus, negative = penalty.

If the girl does not have the trait, the entry has no effect.

Jobs without a performance.xml are pure-effect jobs: the performance score is always 0, so there is no performance tier (and a wage.xml would earn nothing). Use this shape only when the shift outcome does not vary by quality, such as pure rest.

Performance-banded narration requires this file. If your messages/work.xml gates any <Text> on a <Performance> band (<Performance ge="245"/>, <Performance ge="100" le="144"/>, and so on), you must ship a performance.xml. Without one the score is always 0, so every shift matches only the lowest band (<Performance le="..."/>) and the higher bands never fire. This catches even simple household jobs: a cook or cleaner whose narration should read better when she does well still needs a performance.xml to compute that score. “Pure-effect” and “performance-banded” are mutually exclusive.


Converts the performance score into gold earned this shift. Uses a piecewise linear curve: the engine finds which two breakpoints the score falls between and interpolates linearly. For the full picture of how wages compose with girl Ask Prices, tips, and trait modifiers, see pricing.md.

<?xml version="1.0" encoding="UTF-8"?>
<Wage currency="gold">
<Curve type="piecewise">
<Point perf="245" wage="155"/>
<Point perf="185" wage="95"/>
<Point perf="145" wage="55"/>
<Point perf="100" wage="15"/>
<Point perf="70" wage="-5"/>
<Point perf="0" wage="-15"/>
</Curve>
</Wage>
Attribute Notes
currency="gold" Only gold is supported in v1. Required.
type="piecewise" Only piecewise is supported in v1. Required.
perf on <Point> Performance score at this breakpoint. Points must be listed in descending order.
wage on <Point> Gold earned at this performance. Negative values deduct from the brothel (costs the player money).

A job without wage.xml earns no gold. This is correct for training and household-service jobs.


Controls skill and stat XP distributed at the end of each shift. XP is shared across the declared entries proportional to their weights.

<?xml version="1.0" encoding="UTF-8"?>
<Gains xp="15" baseSkill="3">
<Skill name="Service" weight="3"/>
<Skill name="Performance" weight="2"/>
<Stat name="Charisma" weight="1"/>
</Gains>
Attribute / element Notes
xp on <Gains> Total XP distributed this shift, before weights.
baseSkill on <Gains> Flat skill-point floor. Added to each entry regardless of XP.
<Skill name="..." weight="..." Max="..."> A skill that receives XP. Max caps the resulting skill value; further XP into this entry is discarded once the cap is reached.
<Stat name="..." weight="..."> A stat that receives XP.
<GainTrait> / <LoseTrait> Optional. Grant or remove a trait via a progress accumulator. Key attributes: trait= (required), threshold= (default 1000), amount= (default 100). The trait fires after enough qualifying shifts. Can include a <When> gate so only matching shifts count. Progress is visible to players in Girl Details. See gains.md for the full reference and examples.

A job without gains.xml grants no skill XP. Jobs that are purely for rest or housework (cook, rest) typically omit this file.


Shift-end message variants. The engine picks one eligible entry at random (weighted) and shows it in the turn summary.

<?xml version="1.0" encoding="UTF-8"?>
<Bank id="work">
<Text id="barmaid.work.perfect.1" weight="1">
<When><Performance ge="245"/></When>
</Text>
<Text id="barmaid.work.great.1" weight="2">
<When><Performance ge="185" le="244"/></When>
</Text>
<Text id="barmaid.work.ok.1" weight="2">
<When><Performance ge="100" le="144"/></When>
</Text>
</Bank>
Attribute Notes
id on <Bank> Always work for shift messages.
id on <Text> Must match an entry in text/en.xml. Convention: <jobid>.work.<tier>.<n>.
weight Relative probability. Higher = more likely to be selected.
Updates="..." Optional shorthand for in-line stat / earnings adjustments fired together with the message. Semicolon-separated list of <Name><op><Value> entries, e.g. Updates="Tiredness+=5;Tips+=10". Recognised aliases on the earnings side: Tips= and Wages=.
Image="..." Optional. Overrides the job’s <DefaultImage> for this single outcome (1.15.5+). Same vocabulary as <DefaultImage>: any image-catalog tag name (sex, oral, anal, massage, wait, ecchi, etc.). Empty or omitted: the job’s <DefaultImage> is used. Unknown tag names degrade to profile the same way <DefaultImage> does, so a typo can’t brick a shift.
<When> child If present, this variant only fires when the condition is satisfied. Multiple eligible variants are drawn from using their weights.

Every job should have at least one entry with no <When> (or a <When> that always matches), so the turn summary always has something to show.

A single message bag can paint different portraits for different outcomes. The Masseuse bag is the canonical example: a clean shift shows the massage portrait, a happy-ending variant shows sex or oral, a refusal arm shows refuse.

<Bank id="work">
<!-- Default outcome: uses the job's <DefaultImage> (massage). -->
<Text id="masseuse.work.great.1" weight="3">
<When><Performance ge="185"/></When>
</Text>
<!-- Horny variant: paints "oral" instead of "massage". -->
<Text id="masseuse.work.horny.oral.1" weight="1" Image="oral">
<When>
<Performance ge="185"/>
<Stat name="Libido" ge="80"/>
</When>
</Text>
</Bank>

Image= is per-<Text>, never per-<Bank>. Leave it off and you fall back to <DefaultImage>. Set it to any image-catalog tag and that outcome paints the matching portrait instead. Unknown tag names degrade to profile, just like an unknown <DefaultImage>.


The refusal bag. When a girl refuses (a customer on a customer-facing job, or the whole shift), the engine picks one eligible line from here instead of a work.xml line. Same authoring shape as work.xml – a <Bank> of weighted <Text> variants, each with an optional <When> gate – so everything you know from shift messages carries straight over.

<?xml version="1.0" encoding="UTF-8"?>
<Bank>
<!-- Baseline pool: ungated lines so every refusal has something to show. -->
<Text weight="3">
${name} crossed her arms and refused to follow the customer to a room.
</Text>
<!-- Personality overlays: gate on traits, refusal axes, or the reason. -->
<Text weight="2">
<When><Trait id="Noble"/></When>
${name} regarded the customer the way one regards mud on a clean floor, and informed him he had mistaken her for something purchasable.
</Text>
<Text weight="2">
<When><Dignity ge="70"/></When>
${name} drew herself up, looked the customer dead in the eye, and refused. He thought better of pressing the point.
</Text>
<Text weight="2">
<When><Obedience le="20"/></When>
${name} spat at the customer's feet and stormed off. There was no convincing her.
</Text>
<Text weight="2">
<When><RefusalReason name="fear"/></When>
${name} cowered in the corner until the customer cursed and left.
</Text>
</Bank>
Attribute Notes
id on <Bank> Optional; the engine identifies the bag by filename (refuses.xml).
<Text> body Author the prose inline (above), or use id= plus a matching text/en.xml entry exactly like work.xml if you want it localized.
weight Relative probability among eligible lines. Keep the ungated baseline lines at higher weights so they stay dominant when no overlay fires.
Image="..." Optional per-line portrait override (e.g. profile, bdsm); defaults to the refuse image type for the bag.
<When> child Gates the line. A refusal bag has the full <When> grammar plus <RefusalReason> and the <Dignity> / <Fear> / <Obedience> shorthands (see when-conditions.md).

What a refusal line can key on:

  • Personality traits<Trait id="Iron Will"/>, <Trait id="Shy"/>, <Trait id="Aggressive"/>, <Trait id="Noble"/>, so a proud girl refuses differently than a timid one.
  • Character-state axes<Dignity ge="70"/> (proud), <Obedience le="20"/> (defiant), <Fear ge="60"/> (afraid of you), <Stat name="Happiness" le="20"/> (despairing), <Stat name="Tiredness" ge="80"/> (exhausted), <Stat name="PCHate" ge="70"/> (resentful of you).
  • The inferred reason<RefusalReason name="dignity|rebellion|fear|hate|daunted|disease|orientation|beast|generic"/>, so the prose matches why she balked.
  • A specific girl<Girl name="..."/> for per-character refusal lines (a character pack can ship her own).

<Performance> gates do not belong here: the shift’s performance score is not computed before a refusal, so a <Performance> gate in a refusal bag silently never matches.

The whore jobs ship a refusal bag today; other customer-facing jobs gain their own refuses.xml as the refusal model extends to them. Until a job has its own bag (or while a <When> matches nothing), the engine falls back to a generic refusal line, so a partial bag is always safe.


Optional fragment banks for composing a message from interchangeable phrases instead of writing every combination out by hand. A bank is a named pool of short prose fragments; a work.xml (or refuses.xml) line drops a ${pick:<bank-id>} slot into its body, and the engine fills each slot independently when the message fires.

Composition is optional and additive – it does not replace full authored messages. It lets one part of an otherwise hand-written sentence vary. Fixed lines and composed lines coexist freely in the same work.xml bag, and most jobs use no banks at all. A composed message is still authored prose; the bank just supplies the variable phrase.

The intended shape is mostly authored prose with one small dynamic slot – roughly 80% fixed sentence, 20% picked fragment. The sentence, tone, punctuation, and any mechanics stay in the top-level message; only the genuinely variable phrase comes from a bank.

<!-- messages/work.xml: the authored line, gated to a performance tier -->
<Text id="escort.work.outing.good" weight="2">
<When><Performance ge="145" le="184"/></When>
</Text>
<!-- text/en.xml: 80% authored, 20% dynamic -->
<Text id="escort.work.outing.good">${name} accompanied her client to ${pick:outing}, kept him easy company all evening, and saw him home satisfied.</Text>

A root <Banks> holds one or more named <Bank id="..."> elements; each is a pool of <Text> fragments. Fragments use the same vocabulary as work.xml lines – an id-ref into text/en.xml, a weight, and a <When> gate – plus a priority tier.

<?xml version="1.0" encoding="UTF-8"?>
<Banks>
<Bank id="song-genre">
<!-- generic tier (priority 0, ungated): the default pool -->
<Text id="barsinger.genre.rock"/>
<Text id="barsinger.genre.classical"/>
<Text id="barsinger.genre.country"/>
<!-- ...seven in total... -->
<!-- trait override (priority 10): wins ~60% of the time when Aggressive -->
<Text id="barsinger.genre.deathmetal" priority="10">
<When><Trait id="Aggressive"/><RandomChance pct="60"/></When>
</Text>
</Bank>
<Bank id="song-quality">
<Text id="barsinger.quality.perfectly"><When><Performance ge="245"/></When></Text>
<Text id="barsinger.quality.well"><When><Performance ge="145" le="184"/></When></Text>
<!-- ...one per performance tier, covering the whole range with no gap... -->
</Bank>
</Banks>
Attribute Notes
id on <Bank> The bank name referenced by ${pick:<id>}.
id on <Text> The fragment’s text/en.xml key, which holds its prose body.
weight Relative probability within the selected priority tier. Default 1.
priority Selection tier. Default 0. See Selection below.
<When> child Gates the fragment, evaluated in the same shift context as the parent message, with the full when-conditions.md grammar.

Inside a work.xml / refuses.xml body (in text/en.xml), ${pick:<bank-id>} is replaced by one fragment chosen from that bank. The pick: prefix is required: it keeps a bank reference distinct from a <Bind> value that happens to share the name. Several slots, and repeats of the same bank, each roll independently:

<Text id="barmaid.work.mix">${name} mixed ${pick:cocktail} and ${pick:cocktail} without spilling a drop.</Text>

${pick:...} is resolved first, before ${name} / ${shift} / bind tokens, so a fragment may itself contain ${name} (interpolated afterwards). A fragment may not contain another ${pick:...} – recursion is rejected at load.

For each slot the engine:

  1. evaluates every fragment’s <When> once and keeps the eligible ones;
  2. finds the highest priority among those eligible;
  3. picks one fragment from that top tier only, by weight.

A higher-priority fragment therefore wins outright when it is eligible, instead of being averaged into the generic pool. This is what makes “60% Death Metal if Aggressive, otherwise a normal genre” work: the override sits at priority="10", gated on <Trait id="Aggressive"/><RandomChance pct="60"/>. When the trait is present and the 60% roll passes it is the only top-tier fragment and wins; otherwise it is ineligible and selection falls back to the priority="0" generics. If nothing is eligible the slot resolves to empty – so always give a bank an ungated generic tier.

The gate syntax is exactly the runtime grammar (see when-conditions.md): chance is <RandomChance pct="60"/> (the attribute is pct=), performance is <Performance ge="245"/> / <Performance ge="185" le="244"/>, traits are <Trait id="..."/>.

Fragment conditions and selection use the RNG supplied to job resolution – the same stream the rest of the shift draws from – not a separate global path. Seeded tests are therefore reproducible; in normal play the stream is the shared game RNG, so composition is as random as any other shift roll.

Fragments are prose only. A fragment may not carry Updates=, Image=, or any effect; the loader rejects a bank that tries. The mechanically meaningful outcome – and its Updates=, applied exactly once – is the top-level work.xml line; the fragment only varies wording. This guarantees that re-wording, re-weighting, or localizing a fragment can never change a shift’s gameplay result. If a scored sub-event needs to vary, model it as a separate top-level outcome, not a fragment.

banks.xml is checked when the job loads; a failure stops the job from registering and logs a [DataJobs] error rather than shipping a broken line. A bank is rejected when:

  • a ${pick:<id>} in a message references a bank that does not exist;
  • a fragment body contains another ${pick:...} (recursion);
  • a fragment carries Updates=, Image=, or an effect (not prose-only);
  • a declared <Bank> has no fragments.
  • BarSinger (resources/jobs/barsinger/) – the complete example: two banks (song-genre x song-quality) composing ${name} sang ${pick:song-genre} ${pick:song-quality}., with trait overrides and performance-gated quality, sitting alongside the job’s existing fixed lines.
  • Escort (resources/jobs/escort/) – the mixed example: a single pure-flavor outing bank fills one slot in otherwise fully-authored lines. Escort’s normal job mechanics (performance, wage, per-shift effects) live in performance.xml / wage.xml / effects.xml – entirely outside the fragments, and untouched by which venue is named.

examples/jobs-cookbook.md has a step-by-step composition recipe.


Localizable text strings. Each <Text id="..."> entry provides the display string for the matching id in messages/work.xml.

<?xml version="1.0" encoding="UTF-8"?>
<Locale lang="en">
<Text id="barmaid.work.perfect.1">${name} was sliding drinks all over the bar without spilling a drop.</Text>
<Text id="barmaid.work.ok.1">${name} made a few mistakes but none of them were lethal.</Text>
</Locale>

The <Text> body recognizes a small placeholder grammar evaluated at message-emission time:

Token Substitution
${name} The girl’s display name.
${shift} The current shift name (e.g. day, night).
${<bind_name>} The integer value of any <Bind> declared earlier in the same <Group> as the message; useful for reporting a derived performance score, a tip total, etc.
${pick:<bank-id>} One randomly selected fragment from the named bank in messages/banks.xml. Resolved before the tokens above, so a fragment may itself contain ${name}. See messages/banks.xml.
$$ A literal $ character.

Unknown tokens are left in place verbatim, so a typo like ${nmae} shows up in the turn summary rather than corrupting the message; this also keeps packs authored against a newer engine readable on an older one.

<Group>
<Bind name="tip" expr="(Charisma + Service) / 20"/>
<Text id="barmaid.work.great.1">${name} pulled in ${tip} extra coins on the ${shift} shift.</Text>
</Group>

This file only holds strings. The logic (weights, conditions) lives entirely in messages/work.xml. Translators only need to touch text/en.xml (or add a text/fr.xml sibling).


  • examples/jobs-cookbook.md: worked recipes (pure-effect, performance+wage, parameterized multi-slot, composed messages)
  • snippets/effects.xml: copy-paste starter for common effect patterns
  • reference/when-conditions.md: full <When> condition language reference