Skip to main content
RapidDev - Software Development Agency
outsystems-tutorial

Aggregate Joins Explained: Only With, With or Without, and With

OutSystems uses three join names instead of SQL terms: 'Only With' = INNER JOIN (only matching rows), 'With or Without' = LEFT JOIN (all rows from left entity plus matches), 'With' = FULL OUTER JOIN (all rows from both entities). The join type defaults based on whether the FK attribute is Mandatory or not.

What you'll learn

  • Identify the three OutSystems join types and their SQL equivalents
  • Change the join type between entities directly in the Aggregate Sources tab
  • Decide which join type to use based on your data relationship and desired output
  • Join three or more entities in a single Aggregate and control each join independently
  • Diagnose why an Aggregate returns fewer or more rows than expected due to join type
Book a free consultation
4.9Clutch rating
600+Happy partners
17+Countries served
190+Team members
Intermediate10 min read20-30 minOutSystems 11 and ODCMarch 2026RapidDev Engineering Team
TL;DR

OutSystems uses three join names instead of SQL terms: 'Only With' = INNER JOIN (only matching rows), 'With or Without' = LEFT JOIN (all rows from left entity plus matches), 'With' = FULL OUTER JOIN (all rows from both entities). The join type defaults based on whether the FK attribute is Mandatory or not.

OutSystems Join Terminology vs SQL

When you add multiple entities to an Aggregate's Sources tab, OutSystems automatically creates a join between them. The join type determines which rows appear in the results. OutSystems uses its own English names for joins rather than SQL terms, which causes confusion for developers with SQL backgrounds.

The key insight: the default join type is determined by whether the FK attribute (the link between entities) is Mandatory. Mandatory FK = Only With (INNER JOIN). Optional FK = With or Without (LEFT JOIN). You can always override the default by clicking the join indicator in the Aggregate editor.

Prerequisites

  • At least two related entities in your module (e.g., Employee with a DepartmentId FK to Department)
  • Familiarity with creating Aggregates (see outsystems-aggregate-tutorial)
  • Understanding of basic entity relationships (see outsystems-entities-relationships)

Step-by-step guide

1

Open an Aggregate and add two related entities

Open an existing Aggregate or create a new one: right-click your screen → Fetch data from Database. In the Sources tab, drag the Employee entity from the Data panel on the right. Then drag the Department entity. When entities have a defined relationship (Employee.DepartmentId references Department.Id), OutSystems automatically creates a join between them and shows a link line between the two entity boxes in the Sources canvas. If you add two unrelated entities, OutSystems creates a CROSS JOIN (cartesian product) — every employee row combined with every department row. This is almost never what you want. Always ensure related entities have a defined FK relationship before joining them in an Aggregate.

Expected result: Sources tab shows Employee and Department entity boxes connected by a line with a join indicator symbol between them.

2

Read the join indicator and understand the default type

Look at the join indicator between Employee and Department. It shows either: - A filled circle on both ends: Only With (INNER JOIN) - A filled circle on the Employee side, empty circle on the Department side: With or Without (LEFT JOIN) - Empty circles on both ends: With (FULL OUTER JOIN) The default is determined by the FK's Is Mandatory property: - DepartmentId Is Mandatory = Yes → join defaults to Only With - DepartmentId Is Mandatory = No → join defaults to With or Without Hover over the join indicator to see a tooltip confirming the join type and which entities are on each side.

Expected result: You can visually identify the current join type from the indicator symbols. Hovering confirms the tooltip matches your expectation.

3

Use Only With (INNER JOIN) for required relationships

Click the join indicator between Employee and Department. A dropdown appears with three options: Only With, With or Without, With. Select 'Only With'. SQL equivalent: `INNER JOIN Department ON Employee.DepartmentId = Department.Id` Behavior: ONLY employees who have a matching Department row are returned. If any employee's DepartmentId does not match an existing Department.Id (orphaned FK), that employee is excluded from results. Use Only With when: - Every employee MUST belong to a department (mandatory relationship) - You want to display department name alongside employee data and all employees have departments - You need to filter by a department attribute (adding a department filter auto-changes unrelated joins to Only With)

typescript
1/* Only With = INNER JOIN — SQL generated by OutSystems */
2SELECT
3 Employee.Id,
4 Employee.FirstName,
5 Employee.LastName,
6 Department.Name AS DepartmentName
7FROM Employee
8INNER JOIN Department ON Employee.DepartmentId = Department.Id

Expected result: Aggregate preview shows only employees with valid Department records. The row count may be lower than the total Employee count if any employees have null or invalid DepartmentIds.

4

Use With or Without (LEFT JOIN) for optional relationships

Click the join indicator → select 'With or Without'. SQL equivalent: `LEFT JOIN Department ON Employee.DepartmentId = Department.Id` Behavior: ALL employees are returned. Employees with a matching department show the department's data. Employees without a matching department (DepartmentId is null or orphaned) return empty/default values for all Department attributes. In OutSystems expressions, Department attributes for unmatched rows return their default values: Text = "", Integer = 0, Boolean = false, Date = #1900-01-01#, Identifier = NullIdentifier(). Use With or Without when: - Employees can optionally be assigned to a department (optional FK) - You want to show all employees on a list, with department name where available - Building a report that should include ALL employees regardless of department assignment - Checking which employees have no department assigned (filter where DepartmentId = NullIdentifier())

typescript
1/* With or Without = LEFT JOIN — SQL generated by OutSystems */
2SELECT
3 Employee.Id,
4 Employee.FirstName,
5 Employee.LastName,
6 ISNULL(Department.Name, '') AS DepartmentName -- default for unmatched rows
7FROM Employee
8LEFT JOIN Department ON Employee.DepartmentId = Department.Id
9
10/* In expressions: show 'Unassigned' for employees without a department */
11If(Department.Name = "", "Unassigned", Department.Name)

Expected result: Aggregate preview shows ALL employees. Rows with no matching department show empty strings for Department.Name. The row count equals the total Employee count.

5

Use With (FULL OUTER JOIN) and understand when it is needed

Click the join indicator → select 'With'. SQL equivalent: `FULL OUTER JOIN Department ON Employee.DepartmentId = Department.Id` Behavior: ALL rows from BOTH entities appear. This includes: - Employees with a matching department (both sides populated) - Employees without a matching department (Department columns empty) - Departments with no employees (Employee columns empty) This is the least commonly used join type. Typical use case: a reconciliation report showing both orphaned employees AND empty departments in a single result set. IMPORTANT: With (FULL OUTER JOIN) is not supported by all databases. O11 on SQL Server supports it. ODC on Aurora PostgreSQL also supports it. However, it is significantly heavier than INNER or LEFT JOIN — only use it when you genuinely need rows from both sides.

typescript
1/* With = FULL OUTER JOIN — SQL generated by OutSystems */
2SELECT
3 Employee.Id,
4 Employee.FirstName,
5 Department.Id AS DeptId,
6 Department.Name AS DepartmentName
7FROM Employee
8FULL OUTER JOIN Department ON Employee.DepartmentId = Department.Id
9/* Returns: matched pairs + employees without dept + depts without employees */

Expected result: Aggregate preview shows all employees AND all departments. Rows for departments with no employees have empty Employee columns. Row count is higher than using Only With or With or Without.

6

Join three entities and control each join independently

Drag a third entity into the Sources tab — for example, add 'EmploymentStatus' to the existing Employee + Department aggregate. OutSystems creates a second join: Employee → EmploymentStatus (since Employee has a StatusId FK to EmploymentStatus). Each join has its own indicator and can be set independently. Click each join indicator to set the type: - Employee ↔ Department: With or Without (employees may lack a department) - Employee ↔ EmploymentStatus: Only With (all employees must have a status) When joining three or more entities, the order matters for LEFT JOINs. OutSystems uses the first entity added as the 'left' base table. Subsequent entities join in the order added. TrueChange warning: if you change a join type after adding filters on the joined entity, TrueChange may show a warning that the filter makes the With or Without join behave like Only With — adding a WHERE condition on the optional side eliminates NULLs.

Expected result: Sources tab shows three entity boxes with two independent join indicators. Each can be set to a different join type. Preview data reflects the combination of both joins.

Complete working example

EmployeeWithDepartment_JoinReference.outsystems
1/* ===================================================
2 Aggregate: GetEmployeesWithDepartment
3 Three entities: Employee, Department, EmploymentStatus
4 =================================================== */
5
6/* Sources tab configuration */
7Entity 1: Employee (base / left entity)
8Join 1: With or Without /* LEFT JOIN — optional department */
9Entity 2: Department
10
11Join 2: Only With /* INNER JOIN — all employees have a status */
12Entity 3: EmploymentStatus
13
14/* Effective SQL generated (O11 / SQL Server) */
15SELECT
16 Employee.Id,
17 Employee.FirstName,
18 Employee.LastName,
19 ISNULL(Department.Name, '') AS DepartmentName,
20 EmploymentStatus.Label AS StatusLabel
21FROM
22 OSSYS_MODULE.Employee
23 LEFT JOIN OSSYS_MODULE.Department
24 ON Employee.DepartmentId = Department.Id
25 INNER JOIN OSSYS_MODULE.EmploymentStatus
26 ON Employee.StatusId = EmploymentStatus.Id
27
28/* Calculated attribute: DepartmentDisplay */
29Expression: If(Department.Name = "", "(No Department)", Department.Name)
30
31/* Filter: only active employees */
32Employee.IsActive = True
33
34/* Sorting */
35Employee.LastName Ascending
36
37/* Max Records: 50 */
38
39/* Accessing in action flow */
40GetEmployeesWithDepartment.List /* RecordList */
41GetEmployeesWithDepartment.List.Current.Employee.FirstName /* field access */
42GetEmployeesWithDepartment.List.Current.Department.Name /* joined field */
43GetEmployeesWithDepartment.List.Current.EmploymentStatus.Label /* joined field */
44GetEmployeesWithDepartment.Count /* total count */

Common mistakes

Why it's a problem: Adding an unrelated entity to an Aggregate without a FK relationship, creating a cartesian product

How to avoid: Before adding an entity to an Aggregate's Sources tab, ensure it has a FK relationship defined in the Data tab. An unrelated entity join creates a row for every combination (N × M rows). If OutSystems shows no join indicator line after adding the entity, check your entity relationships and define the FK attribute first.

Why it's a problem: Using Only With (INNER JOIN) when employees without departments should appear in the results

How to avoid: Change the join type to With or Without (LEFT JOIN). In the Aggregate editor Sources tab, click the join indicator between Employee and Department and select 'With or Without'. Verify in the preview pane that the row count increases to include employees with null DepartmentIds.

Why it's a problem: Filtering on a With or Without joined entity column and wondering why LEFT JOIN rows disappear

How to avoid: Adding a filter like `Department.Name <> ""` on a LEFT JOIN entity eliminates all rows where Department.Name is NULL (i.e., employees without a department). To keep them, use: `Department.Name = FilterDeptName OR FilterDeptName = ""` — this makes the filter optional when no department is selected, preserving all rows.

Why it's a problem: Expecting the 'With' (FULL OUTER JOIN) type to be available in all database configurations

How to avoid: FULL OUTER JOIN works on SQL Server (O11) and Aurora PostgreSQL (ODC). If you are using an external Oracle or MySQL database in O11, FULL OUTER JOIN may not be supported or may behave differently. Test with your specific database configuration and consider rewriting as two queries (LEFT JOIN + anti-join UNION) if portability is needed.

Best practices

  • Default to With or Without (LEFT JOIN) for optional relationships and Only With (INNER JOIN) for mandatory ones — align join types with your entity's Is Mandatory FK setting to avoid surprises.
  • Never add a WHERE filter on a LEFT JOIN entity's column unless you intend to exclude NULL rows — doing so silently converts a LEFT JOIN to an INNER JOIN and may exclude records you expect to see.
  • When your Aggregate returns fewer rows than expected, check join types first — an accidental Only With (INNER JOIN) on an optional FK is the most common cause of missing rows in OutSystems.
  • Limit joins to 3-4 entities per Aggregate for readability and performance. Complex joins with 6+ entities are a signal to split the query into two aggregates or use an Advanced SQL query.
  • Use the Aggregate preview pane to verify row counts after changing join types — verify that the count matches your expectation (e.g., employee count with LEFT JOIN should equal total employees).
  • With (FULL OUTER JOIN) is rarely needed. Before using it, ask whether two separate Aggregates combined with a Union in Advanced SQL might be more readable and maintainable.

Still stuck?

Copy one of these prompts to get a personalized, step-by-step explanation.

ChatGPT Prompt

I'm building an OutSystems O11 Reactive Web app. I have an Employee entity with a DepartmentId FK (optional — not all employees have departments). I need an Aggregate that returns ALL employees with their department name where available, showing 'No Department' for employees without one. What join type should I use, and how do I write the conditional expression for the department name column?

OutSystems Prompt

In my OutSystems Aggregate, I have Employee, Department, and Project entities. Employee has a mandatory FK to Department and an optional FK to Project. Configure the join types correctly and add a calculated attribute showing the project name or 'Unassigned' when the employee has no project.

Frequently asked questions

What is the OutSystems equivalent of a SQL INNER JOIN?

Only With is OutSystems' INNER JOIN equivalent. It returns only rows where a matching record exists in both entities. In the Aggregate Sources tab, click the join indicator between your entities and select 'Only With'. This is the default when the FK attribute is set to Is Mandatory = Yes.

Why does my Aggregate return zero rows when I add a second entity?

This almost always means the join type is set to Only With (INNER JOIN) but there are no matching rows. Check whether the FK attribute (e.g., DepartmentId) on your records actually contains a valid value matching a Department.Id. Open the Aggregate preview pane and look at the join — if the first entity has rows but the count drops to zero after joining, the FK values don't match any records in the joined entity. Also verify you haven't accidentally created a CROSS JOIN by adding an unrelated entity.

Can I join the same entity twice in one Aggregate, for example joining Employee twice to get a Manager name?

Yes. Drag the Employee entity into Sources a second time — OutSystems creates a second instance (Employee2). You can then join Employee2 on Employee.ManagerId = Employee2.Id using a manual join condition. Click the join between Employee and Employee2, then select the custom join condition option. This is an advanced pattern and can be confusing — consider using a calculated attribute in a Server Action if the self-join becomes complex.

How do OutSystems join types behave differently in ODC compared to O11?

The join behavior and OutSystems naming (Only With, With or Without, With) is identical in both O11 and ODC Studio. The underlying SQL differs — O11 generates SQL Server T-SQL and ODC generates PostgreSQL SQL — but the semantics are the same. The one practical difference: FULL OUTER JOIN (With) syntax varies between SQL Server and PostgreSQL, but OutSystems handles this translation automatically, so you do not need to change your Aggregate configuration when migrating.

RapidDev

Talk to an Expert

Our team has built 600+ apps. Get personalized help with your project.

Book a free consultation

Need help with your project?

Our experts have built 600+ apps and can accelerate your development. Book a free consultation — no strings attached.

Book a free consultation

We put the rapid in RapidDev

Need a dedicated strategic tech and growth partner? Discover what RapidDev can do for your business! Book a call with our team to schedule a free, no-obligation consultation. We'll discuss your project and provide a custom quote at no cost.