> ## Documentation Index
> Fetch the complete documentation index at: https://docs.twine.se/llms.txt
> Use this file to discover all available pages before exploring further.

# Simployer One

export const IntegrationBlurb = ({children}) => {
  const kids = Array.isArray(children) ? children : [children];
  const hasContent = kids.some(child => {
    if (child == null || child === false) return false;
    if (typeof child === 'string') return child.trim().length > 0;
    return true;
  });
  if (!hasContent) return null;
  return <blockquote style={{
    borderLeft: '3px solid rgb(var(--primary-light) / .3)',
    margin: '0 0 32px 0',
    padding: '10px 16px',
    background: 'rgb(var(--primary-light) / .05)',
    borderRadius: '0 6px 6px 0',
    fontStyle: 'italic'
  }}>
      {children}
    </blockquote>;
};

export const IntegrationMeta = ({homepageUrl, apiReferenceUrls, contactEmail, transport, status, domains, capabilities, notes}) => {
  const hasLinks = homepageUrl || apiReferenceUrls && apiReferenceUrls.length > 0 || contactEmail;
  const hasDomains = domains && domains.length > 0;
  const hasTransport = !!transport;
  const hasStatus = status === 'planned' || status === 'deprecated';
  const hasCapabilities = capabilities && capabilities.length > 0;
  const hasNotes = !!notes;
  if (!hasLinks && !hasDomains && !hasTransport && !hasStatus && !hasCapabilities && !hasNotes) return null;
  const pill = (bg, color, text) => <span style={{
    display: 'inline-flex',
    alignItems: 'center',
    fontSize: '11px',
    fontWeight: 600,
    lineHeight: 1,
    padding: '3px 8px',
    borderRadius: '999px',
    background: bg,
    color: color,
    whiteSpace: 'nowrap'
  }}>
  {text}
</span>;
  const capabilityColors = {
    'read/write': ['var(--colors-accent-light, #ede9fe)', 'var(--colors-accent, #7c3aed)'],
    'read-write': ['var(--colors-accent-light, #ede9fe)', 'var(--colors-accent, #7c3aed)'],
    'read': ['rgb(var(--primary-light) / .15)', 'var(--colors-content-secondary, #6b7280)'],
    'write': ['rgb(var(--primary-light) / .15)', 'var(--colors-content-secondary, #6b7280)']
  };
  const transportColors = {
    'file': ['var(--colors-warning-light, #fef3c7)', 'var(--colors-warning, #b45309)'],
    'api': ['var(--colors-accent-light, #ede9fe)', 'var(--colors-accent, #7c3aed)'],
    'webhook': ['var(--colors-success-light, #dcfce7)', 'var(--colors-success, #16a34a)']
  };
  const statusConfig = {
    'planned': {
      colors: ['var(--colors-warning-light, #fef3c7)', 'var(--colors-warning, #b45309)'],
      text: 'Planned'
    },
    'deprecated': {
      colors: ['rgb(var(--primary-light) / .15)', 'var(--colors-content-secondary, #6b7280)'],
      text: 'Deprecated'
    }
  };
  const label = text => <span style={{
    fontSize: '11px',
    fontWeight: 600,
    textTransform: 'uppercase',
    letterSpacing: '0.05em',
    color: 'var(--colors-content-secondary, #6b7280)',
    whiteSpace: 'nowrap',
    alignSelf: 'start',
    paddingTop: '2px'
  }}>
  {text}
</span>;
  const cell = children => <div style={{
    display: 'flex',
    alignItems: 'center',
    flexWrap: 'wrap',
    gap: '8px'
  }}>
  {children}
</div>;
  return <>
<style>{`
  .integration-meta-link {
    font-size: 13px;
    color: var(--colors-primary);
    text-decoration: none !important;
    border-bottom: none !important;
    box-shadow: none !important;
    position: relative;
  }
  .integration-meta-link:hover {
    text-decoration: none !important;
    border-bottom: none !important;
    box-shadow: none !important;
  }
  .integration-meta-link::after {
    content: '';
    position: absolute;
    left: 0;
    bottom: -1px;
    width: 100%;
    height: 1px;
    background: rgb(var(--primary-light));
    opacity: 0.5;
    transition: opacity 0.15s;
  }
  .integration-meta-link:hover::after {
    opacity: 1;
  }
`}</style>
<div style={{
    display: 'grid',
    gridTemplateColumns: 'max-content 1fr',
    alignItems: 'center',
    gap: '8px 16px',
    padding: '14px 16px',
    borderRadius: '8px',
    background: 'rgb(var(--primary-light) / .05)',
    border: '1px solid rgb(var(--primary-light) / .1)',
    marginBottom: '32px'
  }}>
{hasLinks && <>
{label('Links')}
{cell(<>
{homepageUrl && <a href={homepageUrl} target="\_blank" rel="noopener noreferrer" className="integration-meta-link">
Website
</a>}
{apiReferenceUrls && apiReferenceUrls.map(({url, label: l}) => <a key={url} href={url} target="\_blank" rel="noopener noreferrer" className="integration-meta-link">
{l || 'API Reference'}
</a>)}
{contactEmail && <a href={`mailto:${contactEmail}`} className="integration-meta-link">
{contactEmail}
</a>}
</>)}
</>}

      {hasStatus && <>
        {label('Status')}
        {cell(pill(...statusConfig[status].colors, statusConfig[status].text))}
      </>}

      {hasTransport && <>
        {label('Transport')}
        {cell(pill(...transportColors[transport] || transportColors['api'], transport === 'file' ? 'File' : transport === 'webhook' ? 'Webhook' : 'API'))}
      </>}

      {hasDomains && <>
        {label('Domains')}
        {cell(domains.map(({domain, capability}) => {
    const [capBg, capColor] = capabilityColors[capability] || capabilityColors['read'];
    return <span key={domain} style={{
      display: 'inline-flex',
      alignItems: 'stretch',
      borderRadius: '999px',
      overflow: 'hidden',
      whiteSpace: 'nowrap',
      fontSize: '12px',
      fontWeight: 600,
      lineHeight: 1
    }}>
              <span style={{
      padding: '4px 8px',
      background: 'rgb(var(--primary-light) / .3)',
      color: 'inherit',
      fontFamily: 'monospace'
    }}>{domain}</span>
              <span style={{
      padding: '4px 8px',
      background: capBg,
      color: capColor
    }}>{capability}</span>
            </span>;
  }))}
      </>}

      {hasCapabilities && <>
        {label('Capabilities')}
        <div style={{
    display: 'flex',
    flexDirection: 'column',
    gap: '4px'
  }}>
          {capabilities.map(({label: capLabel, supported}) => <div key={capLabel} style={{
    display: 'flex',
    alignItems: 'center',
    gap: '8px',
    fontSize: '13px'
  }}>
              <span style={{
    fontWeight: 700,
    fontSize: '12px',
    color: supported ? 'var(--colors-success, #16a34a)' : 'var(--colors-error, #dc2626)',
    width: '12px',
    flexShrink: 0
  }}>{supported ? '✓' : '✗'}</span>
              <span style={{
    color: 'inherit'
  }}>{capLabel}</span>
            </div>)}
        </div>
      </>}

      {hasNotes && <>
        {label('Notes')}
        <span style={{
    fontSize: '13px',
    color: 'var(--colors-content-secondary, #6b7280)',
    fontStyle: 'italic'
  }}>{notes}</span>
      </>}
    </div>

</>;
};

export const IntegrationHeader = ({name, category, logoUrl}) => <div style={{
  display: 'flex',
  alignItems: 'center',
  gap: '16px',
  marginBottom: '24px'
}}>
    {logoUrl && <div style={{
  width: '64px',
  height: '64px',
  borderRadius: '12px',
  background: '#ffffff',
  display: 'flex',
  alignItems: 'center',
  justifyContent: 'center',
  flexShrink: 0,
  boxShadow: '0 0 0 1px rgba(0,0,0,0.08)'
}}>
        <img src={logoUrl} alt={`${name} logo`} style={{
  width: '40px',
  height: '40px',
  objectFit: 'contain'
}} />
      </div>}
    <div>
      <h1 style={{
  margin: 0
}}>{name}</h1>
      <span style={{
  color: 'var(--colors-content-secondary)',
  fontSize: '14px'
}}>
        {category}
      </span>
    </div>
  </div>;

<IntegrationHeader name="Simployer One" category="HRIS" logoUrl="https://kbmni.upcloudobjects.com/public/systemlogos/simployer-one-logo.svg" />

<IntegrationMeta
  homepageUrl="https://www.simployer.com"
  apiReferenceUrls={[
{
  label: 'Simployer One API',
  url: 'https://docs.alexishr.com/',
},
]}
  transport="api"
  domains={[
{ domain: "employee_competence", capability: "read/write" },
{ domain: "competence", capability: "read/write" },
{ domain: "org_unit", capability: "read" },
{ domain: "time_report", capability: "read/write" },
{ domain: "employee", capability: "read/write" },
]}
  capabilities={[
{ label: 'Read / Create / Update salaries', supported: true },
{ label: 'Read / Create / Update employment periods', supported: true },
{ label: 'Read Organizational Units', supported: true },
]}
/>

<IntegrationBlurb>
  Simployer is a Nordic-built HR platform that unifies the entire employee journey - from recruitment to offboarding - on a single system, combining intuitive people management with AI-powered tools, employee engagement features, and compliance expertise.
</IntegrationBlurb>

<Note>
  This page is part of an early release. Detailed information about Twine's integration with this system is being prepared and will be filled in shortly.
</Note>

## Competences

Simployer One supports both competence domains, in both directions - Twine can read its catalogue and assignments, and write to them. See [Competence](/platform/data-model/competence) for the data model and the general rules; this section covers what is specific to Simployer One.

### Domain mappings

Simployer One keeps assignments as their own records, so the full setup is **three domain mappings**: Employee, Competence, and Competence Assignment. They are configured separately and should run in that order, since an assignment can only be written once both its employee and its competence are mapped.

<Tip>
  Make the competence [sync trigger](/platform/sync-triggers) a child of the employee trigger, and the assignment trigger a child of the competence one. The ordering then takes care of itself.
</Tip>

Nothing flows until the relevant fields are mapped - competence fields are not part of the default property mappings for this integration, so an existing Simployer One integration is unaffected until you map them deliberately.

### Fixed value sets

Two fields accept only a fixed set of values, and Simployer One rejects anything else. Map your own values onto them with the [Data Engine](/data-engine), the same way as for any other value translation.

| Field              | Accepted values                                                          |
| ------------------ | ------------------------------------------------------------------------ |
| Competence `type`  | `SKILL`, `CERTIFICATE`                                                   |
| Assignment `level` | `Expert`, `Advanced`, `Proficient`, `Intermediate`, `Beginner`, `Absent` |

A level outside that set is reported as an anomaly and skipped - the assignment itself is still written, just without a level.

### Assignments have no dates

A Simployer One assignment records that an employee holds a competence, and nothing more - no validity period, no sequence. One employee holding one competence is one record.

That has a visible consequence for re-certification. Where Twine holds several assignments of the same competence for an employee, they collapse onto the single Simployer One record, and the one with the **newest `valid_from`** supplies the level. The full history stays in Twine; Simployer One sees the current state.

### Certificate documents

Simployer One assignments can reference a document held in Simployer One - a certificate PDF, typically. The document id is available as a mappable field on the assignment, so if you have the id, put it in a [custom property](/platform/data-model/competence#custom-properties) and map it across.

<Note>
  The document id is applied **when the assignment is created**. Simployer One does not support updating an existing assignment, so mapping the field later will not attach a document to assignments that already exist.
</Note>

### Competences that already exist

Where Simployer One already holds competences, link the two catalogues with the [Entity Matcher](/backoffice/configuration/entity-matcher) before the first sync - competences are matched on name by default.

An unmapped competence is created rather than matched by name. If a competence about to be created turns out to have a same-named twin in Simployer One that is not linked to Twine, the default is to **skip it, along with the assignment that needed it, and report an anomaly** - rather than silently create a second copy or silently adopt one. The behaviour can be changed per integration in the integration's settings if you would rather it adopt the existing entry, or create regardless.

### Limitations

* **Upsert-only.** Twine does not delete competences or assignments in Simployer One. Removing one is done there directly.
* **No assignment updates.** Simployer One has no way to modify an existing assignment, so only the level (a separate record) changes after creation.
* Competence **scopes** - Simployer One's "required for this office/team" configuration - are not read or written by Twine.

<Card title="Simployer One changelog" icon="list" href="/changelog/alexis_hr">
  Notable changes to the Simployer One integration over time.
</Card>
